JUC系列之线程安全的List、Set、Map(讲明白其中的原因)


前言

  • 为什么要说线程安全与线程不安全呢,在大电商平台出来之前,我们都还使用单体应用,一般都部署在单个的容器中,都是一些比较简单的应用,简单的交互,不涉及多线程处理,一般我们使用ArrayList、HashSet、HashMap 都不会有什么问题;
  • 但是是在阿里巴巴、淘宝、天猫、京东、亚马逊等这些大型的网上商城出来之后,由于交易量巨大,在同一时间不可能是单个交易,这也就出现了一个问题,以前这些常用的ArrayList、HashSet、HashMap还能用吗,为什么不能用了?

带着上面的问题让我们一起来学习一下这章的内容。


一、什么是线程安全?

1. 专业定义

  • 多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其它操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。
  • 一个类可者程序所提供的接口对于线程来说是原子操作或者多个线程之间切换不会导致该接口执行结果存在二义性,也就是说我们不用考虑同步的问题。
  • 线程安全问题大多是由全局变量及静态变量引起的,局部变量逃逸也可能导致线程安全问题。

2. 通俗定义

多个线程同时操作同一个变量,不会出现意想不到的结果,那我们就说这个变量是线程安全的。

接触代码少的同学可能还是很难理解,仔细阅读下面的部分就会有所体会了。


二、ArrayList、HashSet、HashMap是线程安全的吗,怎么证明?有什么解决方案?

最常见的面试问题:

  1. ArrayList、HashSet、HashMap是线程安全的吗?
  2. 怎么证明它们不是线程安全的?
  3. 有什么解决方案?

下面我们就给大这一一证明,并给出解决方案

1. ArrayList(如何证明其不是线程安全的)

证明步骤:

  1. 定义一个全局变量ArrayList
  2. 循环开启100个线程,对ArrayList进行操作

代码如下:

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class TestSafeThread {
    
    

    public static void main(String[] args) {
    
    

        // 全局变量ArrayList
        List<String> list = new ArrayList<>();

        // 创建100个线程对ArrayList进行操作
        for(int i = 0; i < 100; i++) {
    
    
            new Thread(() -> {
    
    
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }

}

我们可以在执行结果中看到如下报错信息:

Exception in thread "21" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
	at java.util.ArrayList$Itr.next(ArrayList.java:851)
	at java.util.AbstractCollection.toString(AbstractCollection.java:461)
	at java.lang.String.valueOf(String.java:2994)
	at java.io.PrintStream.println(PrintStream.java:821)
	at TestSafeThread.lambda$main$0(TestSafeThread.java:16)
	at java.lang.Thread.run(Thread.java:748)
Exception in thread "6" Exception in thread "77" java.util.ConcurrentModificationException

以上异常是:多线程并发修改异常,出现这个异常就说明ArrayList不是线程安全的。

2. ArrayList(如何解决)

这里给大家提供三种解决方法:

1) 使用Vector替代

  • 代码如下(示例):
import java.util.List;
import java.util.UUID;
import java.util.Vector;

public class TestSafeThread {
    
    

    public static void main(String[] args) {
    
    

        // 全局变量Vector
        List<String> list = new Vector<>();

        // 创建100个线程对Vector进行操作
        for(int i = 0; i < 100; i++) {
    
    
            new Thread(() -> {
    
    
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }

}

使用Vector之后就不会再出现ConcurrentModificationException,大家可以自行测试,这里就不贴运行之后的结果了。

  • Vector 为什么是线程安全的呢?让我们一起来看一下其源码,此处只展示其add方法:
    /**
     * Appends the specified element to the end of this Vector.
     *
     * @param e element to be appended to this Vector
     * @return {@code true} (as specified by {@link Collection#add})
     * @since 1.2
     */
    public synchronized boolean add(E e) {
    
    
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

从源码中可以看出:

  1. 其add方法是同步方法,它添加了synchronized,来确保其是线程安全的。
  2. 从其注释中的@since 1.2 可知这个方法已经是比较老的方法了,不太建议大家在真实的项目中使用。(当然并不是说它不能用)

2) 使用Collections.synchronizedList(new ArrayList<>())来替代

  • 代码如下:
import java.util.*;

public class TestSafeThread {
    
    

    public static void main(String[] args) {
    
    

        // 全局变量synchronizedList
        List<String> list = Collections.synchronizedList(new ArrayList<>()); 

        // 创建100个线程对synchronizedList进行操作
        for(int i = 0; i < 100; i++) {
    
    
            new Thread(() -> {
    
    
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }

}

使用工具类Collections的synchronizedList()方法执行后也不会出现ConcurrentModificationException,由于执行结果比较多,这里就不贴出来了,大家可自行测试。

  • Collections.synchronizedList()创建出来的ArrayList为什么是线程安全的呢?让我们一起来看一下其源码:
public static <T> List<T> synchronizedList(List<T> list) {
    
    
        return (list instanceof RandomAccess ?
                new SynchronizedRandomAccessList<>(list) :
                new SynchronizedList<>(list));
    }

由上部分源码可看出,其返回的是SynchronizedList, 我们再往上跟踪

static class SynchronizedList<E>
        extends SynchronizedCollection<E>
        implements List<E>

可以看出SynchronizedList 继承了SynchronizedCollection 类,我们继续往上跟踪

static class SynchronizedCollection<E> implements Collection<E>, Serializable

可以看出SynchronizedCollection是实现了Collection,那么关键代码就是在这个类里了,让我们找到add方法看看

final Collection<E> c;  // Backing Collection
final Object mutex;     // Object on which to synchronize

SynchronizedCollection(Collection<E> c) {
    
    
    this.c = Objects.requireNonNull(c);
    mutex = this;
}

public boolean add(E e) {
    
    
    synchronized (mutex) {
    
    return c.add(e);}
}

由此可以看出,其底层是使用synchronized,将其加在了 Object类型的mutex上,与Vector的区别是:Vector加在了方法上。

3) 使用CopyOnWriteArrayList(写时复制)替代【推荐使用】

  • 代码如下:
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

public class TestSafeThread {
    
    

    public static void main(String[] args) {
    
    

        // 全局变量CopyOnWriteArrayList
        List<String> list = new CopyOnWriteArrayList<>(); 
        // 创建100个线程对CopyOnWriteArrayList进行操作
        for(int i = 0; i < 100; i++) {
    
    
            new Thread(() -> {
    
    
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }

}

执行结果这里就不贴出来了,没再出现ConcurrentModificationException,感兴趣的同学可自行测试。

  • 问题来了,为什么CopyOnWriteArrayList是线程安全的呢?带着这个问题让我们一起看一下源码(这里只粘出了部分关键代码):
public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    
    

	final transient ReentrantLock lock = new ReentrantLock();
	
	public CopyOnWriteArrayList() {
    
    
        setArray(new Object[0]);
    }
    
    private E get(Object[] a, int index) {
    
    
        return (E) a[index];
    }
    
    public E get(int index) {
    
    
        return get(getArray(), index);
    }
    
    public boolean add(E e) {
    
    
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
    
    
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
    
    
            lock.unlock();
        }
    }
}

从源码中我们可以看出,其在全局定义了一个ReentrantLock(可重入锁),其add方法内部分使用了锁,并复制后添加。但是其get方法没有加锁,相当于是‘读写分离’

3. HashSet (如何证明其不是线程安全的)

其证明方法和ArrayList是一样的

证明步骤:

  1. 定义一个全局变量HashSet
  2. 循环开启100个线程,对HashSet进行操作

代码如下:

public static void main(String[] args) {
    
    
	// 全局变量HashSet
    Set<String> set = new HashSet<>();
    // 创建100个线程对HashSet进行操作
    for(int i = 0; i < 100; i++) {
    
    
    	new Thread(() -> {
    
    
        	set.add(UUID.randomUUID().toString().substring(0, 8));
            System.out.println(set);
        }, String.valueOf(i)).start();
    }
}

执行结果:

Exception in thread "59" java.util.ConcurrentModificationException
	at java.util.HashMap$HashIterator.nextNode(HashMap.java:1437)
	at java.util.HashMap$KeyIterator.next(HashMap.java:1461)
	at java.util.AbstractCollection.toString(AbstractCollection.java:461)
	at java.lang.String.valueOf(String.java:2994)
	at java.io.PrintStream.println(PrintStream.java:821)
	at TestSafeThread.lambda$main$0(TestSafeThread.java:14)
	at java.lang.Thread.run(Thread.java:748)
Exception in thread "61" java.util.ConcurrentModificationException
	at java.util.HashMap$HashIterator.nextNode(HashMap.java:1437)
	at java.util.HashMap$KeyIterator.next(HashMap.java:1461)
	at java.util.AbstractCollection.toString(AbstractCollection.java:461)
	at java.lang.String.valueOf(String.java:2994)
	at java.io.PrintStream.println(PrintStream.java:821)
	at TestSafeThread.lambda$main$0(TestSafeThread.java:14)
	at java.lang.Thread.run(Thread.java:748)

可以看到报了很多 ConcurrentModificationException,说明HashSet不是线程安全的。
扩展:TreeSet、LinkedHashSet 也都是线程不安全的。

4. HashSet(如何解决)

这里提供两种解决方法

1) 使用Collections.synchronizedSet()替代

  • 代码如下:
public static void main(String[] args) {
    
    
	// 全局变量synchronizedSet
    Set<String> set = Collections.synchronizedSet(new HashSet<>()); 
    // 创建100个线程对synchronizedSet进行操作
    for(int i = 0; i < 100; i++) {
    
    
    	new Thread(() -> {
    
    
        	set.add(UUID.randomUUID().toString().substring(0, 8));
            System.out.println(set);
        }, String.valueOf(i)).start();
	}
}

这里就不贴执行结果了,正常执行完成,没有出现ConcurrentModificationException。

  • 看其源代码:
public static <T> Set<T> synchronizedSet(Set<T> s) {
    
    
	return new SynchronizedSet<>(s);
}

static class SynchronizedSet<E>
          extends SynchronizedCollection<E>

由源代码可以看出,其同样是继承了SynchronizedCollection,底层调用的与synchronizedList是同一块代码,这里就不重复说了。

2)使用 CopyOnWriteArraySet(写时复制)替代【推荐使用】

  • 代码如下:
public static void main(String[] args) {
    
    
	// 全局变量CopyOnWriteArraySet
    Set<String> set = new CopyOnWriteArraySet<>(); 
    // 创建100个线程对CopyOnWriteArraySet进行操作
    for(int i = 0; i < 100; i++) {
    
    
    	new Thread(() -> {
    
    
        	set.add(UUID.randomUUID().toString().substring(0, 8));
            System.out.println(set);
        }, String.valueOf(i)).start();
    }
}

执行没有出现任何异常,这里就不贴结果了,感兴趣的同学可自行测试。

  • 看其源码:
public CopyOnWriteArraySet() {
    
    
        al = new CopyOnWriteArrayList<E>();
    }

可以看出其底层是CopyOnWriteArrayList,这里就不重复说明了。

5. HashMap(如何证明其不是线程安全的)

其证明方法和ArrayList是一样的

证明步骤:

  1. 定义一个全局变量HashMap
  2. 循环开启100个线程,对HashMap进行操作

代码如下:

public static void main(String[] args) {
    
    
	// 全局变量HashMap
    Map<String, String> map = new HashMap<>();
    // 创建100个线程对HashMap进行操作
    for(int i = 0; i < 100; i++) {
    
    
    	new Thread(() -> {
    
    
        	map.put(Thread.currentThread().getName().toString(),UUID.randomUUID().toString().substring(0, 8));
            System.out.println(map);
        }, String.valueOf(i)).start();
    }
}

执行结果:

at java.util.HashMap$HashIterator.nextNode(HashMap.java:1437)
	at java.util.HashMap$EntryIterator.next(HashMap.java:1471)
	at java.util.HashMap$EntryIterator.next(HashMap.java:1469)
	at java.util.AbstractMap.toString(AbstractMap.java:554)
	at java.lang.String.valueOf(String.java:2994)
	at java.io.PrintStream.println(PrintStream.java:821)
	at TestSafeThread.lambda$main$0(TestSafeThread.java:15)
	at java.lang.Thread.run(Thread.java:748)
Exception in thread "47" java.util.ConcurrentModificationException
	at java.util.HashMap$HashIterator.nextNode(HashMap.java:1437)
	at java.util.HashMap$EntryIterator.next(HashMap.java:1471)
	at java.util.HashMap$EntryIterator.next(HashMap.java:1469)
	at java.util.AbstractMap.toString(AbstractMap.java:554)
	at java.lang.String.valueOf(String.java:2994)
	at java.io.PrintStream.println(PrintStream.java:821)
	at TestSafeThread.lambda$main$0(TestSafeThread.java:15)
	at java.lang.Thread.run(Thread.java:748)

可以看到同样是出现了ConcurrentModificationException,说明HashMap不是线程安全的。
扩展:LinkedHashMap 继承了HashMap所以也不是线程安全的,TreeMap也不是线程安全的。

6. HashMap(如何解决)

这里同样给出三种解决方法

1)使用HashTable替代

  • 代码如下:
public static void main(String[] args) {
    
    
	// 全局变量Hashtable
    Map<String, String> map = new Hashtable<>(); // new HashMap<>();
    // 创建100个线程对Hashtable进行操作
    for(int i = 0; i < 100; i++) {
    
    
    	new Thread(() -> {
    
    
        	map.put(Thread.currentThread().getName().toString(),UUID.randomUUID().toString().substring(0, 8));
            System.out.println(map);
        }, String.valueOf(i)).start();
    }
}

执行结果没有出现任何异常,想看结果的同学可以自己测试一下。

  • 源码:
public synchronized V put(K key, V value) {
    
    
        // Make sure the value is not null
        if (value == null) {
    
    
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
    
    
            if ((entry.hash == hash) && entry.key.equals(key)) {
    
    
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }

        addEntry(hash, key, value, index);
        return null;
    }

看其源码可知,其在整个put方法上增加了synchronized

2)使用Collections.synchronizedMap()替代

  • 代码如下:
    public static void main(String[] args) {
    
    
        // 全局变量synchronizedMap
        Map<String, String> map = Collections.synchronizedMap(new HashMap<>()); // new Hashtable<>(); // new HashMap<>();

        // 创建100个线程对synchronizedMap进行操作
        for(int i = 0; i < 100; i++) {
    
    
            new Thread(() -> {
    
    
                map.put(Thread.currentThread().getName().toString(),UUID.randomUUID().toString().substring(0, 8));
                System.out.println(map);
            }, String.valueOf(i)).start();
        }

    }
  • 源码:
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
    
    
        return new SynchronizedMap<>(m);
    }

private static class SynchronizedMap<K,V>
        implements Map<K,V>, Serializable {
    
    
		private final Map<K,V> m;     // Backing Map
        final Object      mutex;        // Object on which to synchronize
		public V put(K key, V value) {
    
    
            synchronized (mutex) {
    
    return m.put(key, value);}
        }
}

可以看到其原理和synchronizedList 是一样的,在方法内部添加了 synchronized。

3)使用ConcurrnetHashMap 替代【推荐使用】

  • 代码如下:
    public static void main(String[] args) {
    
    
        // 全局变量ConcurrentHashMap
        Map<String, String> map = new ConcurrentHashMap<>();// Collections.synchronizedMap(new HashMap<>()); // new Hashtable<>(); // new HashMap<>();

        // 创建100个线程对ConcurrentHashMap进行操作
        for(int i = 0; i < 100; i++) {
    
    
            new Thread(() -> {
    
    
                map.put(Thread.currentThread().getName().toString(),UUID.randomUUID().toString().substring(0, 8));
                System.out.println(map);
            }, String.valueOf(i)).start();
        }

    }

执行结果无异常。

  • 源代码:
public V put(K key, V value) {
    
    
        return putVal(key, value, false);
    }

    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
    
    
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
    
    
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    
    
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
    
    
                V oldVal = null;
                synchronized (f) {
    
    
                    if (tabAt(tab, i) == f) {
    
    
                        if (fh >= 0) {
    
    
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
    
    
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
    
    
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
    
    
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
    
    
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
    
    
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
    
    
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

可以看到其在putVal方法中使用了 synchronized 代码。


总结

以上就是关于List、Set、Map 三大集合线程安全与不安全的讲解,有不足的地方还希望大家在评论区进行补充,一起学习。

猜你喜欢

转载自blog.csdn.net/yezhijing/article/details/128216139