1. 集合线程不安全演示
以 ArrayList 为例,我们进入 ArrayList 源码,找到 add() 方法,源码如下
1 2 3 4 5
| public boolean add(E e) { ensureCapacityInternal(size + 1); elementData[size++] = e; return true; }
|
显然,add() 方法没有使用同步互斥,所以在多线程并发时,会出现线程异常,测试代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public static void main(String[] args) { ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) { new Thread(()->{ list.add(UUID.randomUUID().toString().substring(0,8)); System.out.println(list); },String.valueOf(i)).start(); } }
|
会出现如下异常
2. 解决方案
2.1 Vector
1
| List<String> list = new Vector<>();
|
源码如下, 使用 synchronized 修饰,但是这样做效率十分低下且占用资源
1 2 3 4 5 6
| public synchronized boolean add(E e) { modCount++; ensureCapacityHelper(elementCount + 1); elementData[elementCount++] = e; return true; }
|
2.2 Collections
1
| List<String> list = Collections.synchronizedList(new ArrayList<>());
|
进入 Collections 的底层,找到 synchronizedList(List list)
方法,源代码如下,返回指定列表支持的同步(线程安全的)列表
1 2 3 4 5 6 7 8 9 10 11
| public static <T> List<T> synchronizedList(List<T> list) { return (list instanceof RandomAccess ? new SynchronizedRandomAccessList<>(list) : new SynchronizedList<>(list)); }
static <T> List<T> synchronizedList(List<T> list, Object mutex) { return (list instanceof RandomAccess ? new SynchronizedRandomAccessList<>(list, mutex) : new SynchronizedList<>(list, mutex)); }
|
2.3 CopyOnWriteArrayList
使用最多,涉及的底层原理为写时复制技术
1
| List<String> list = new CopyOnWriteArrayList<>();
|
源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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(); } }
|
对比三者来看,Vector 和 Collections 虽然也可以实现同步,但由于这两种方法在底层都使用了 synchronized 重量级锁,使其效率很低,所以对 ArrayList 的同步主要采用 CopyOnWriteArrayList
3. HashSet的线程不安全
HashSet 同时读写时也会出现 ConcurrentModificationException 异常
其 add() 源代码如下, 没有对做同步处理
1 2 3
| public boolean add(E e) { return map.put(e, PRESENT)==null; }
|
其解决方法与 CopyOnWriteArrayList
类似,在 JDK1.8 中,也有一个类叫做 CopyOnWriteArraySet
,其底层代码如下
1 2 3
| public boolean add(E e) { return al.addIfAbsent(e); }
|
通过 Debug 找到了对关键的一个函数,发现其实现方式与 CopyOnWriteArrayList 底层实现方式类似
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
private boolean addIfAbsent(E e, Object[] snapshot) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] current = getArray(); int len = current.length; if (snapshot != current) { int common = Math.min(snapshot.length, len); for (int i = 0; i < common; i++) if (current[i] != snapshot[i] && eq(e, current[i])) return false; if (indexOf(e, current, common, len) >= 0) return false; } Object[] newElements = Arrays.copyOf(current, len + 1); newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } }
|
4. HashMap的线程不安全
HashMap 同时读写时一样会出现 ConcurrentModificationException 异常
进入 HashMap 底层,其 put(K key, V value) 源代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
|
在 JDK1.8 中,也有一个类叫做 ConcurrentHashMap ,实现 HashMap 的同步问题,其底层代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| public V put(K key, V value) { return putVal(key, value, false); }
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; } 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; }
|