面试官: 小伙子你有点眼熟啊,是否是去年来这面试过啊。<br/>
二胖: 啊,没有啊我这是第一次来这。<br/>
面试官: 行,那咱们开始今天的面试吧,刚开始咱们先来点简单的吧,java
里面的容器你知道哪些啊,跟我说一说吧。<br/>
二胖: 好的,java里面常见容器有ArrayList
(线程非安全)、HashMap
(线程非安全)、HashSet
(线程非安全),ConcurrentHashMap
(线程安全)。<br/>
面试官: ArrayList
既然线程非安全那有没有线程安全的ArrayList
列?<br/>
二胖: 这个。。。 好像问到知识盲点了。<br/>
面试官: 那咱们今天的面试就先到这了,我待会还有一个会,后续若有通知人事会联系你的。<br/>
以上故事纯属虚构若有雷同请以本文为主。java
在java里面说到集合容器咱们通常首先会想到的是HashMap
、ArrayList
、HasHSet
这几个容器也是平时开发中用的最多的。
这几个都是非线程安全的,若是咱们有特定业务须要使用线程的安全容器列,面试
HashMap
能够用ConcurrentHashMap
代替。ArrayList
可使用Collections.synchronizedList()
方法(list
每一个方法都用synchronized
修饰) 或者使用Vector
(如今基本也不用了,每一个方法都用synchronized
修饰)或者使用CopyOnWriteArrayList
替代。数组
Collections.synchronizedSet
或者使用CopyOnWriteArraySet
来代替。(CopyOnWriteArraySet为何不叫CopyOnWriteHashSet由于CopyOnWriteArraySet
底层是采用CopyOnWriteArrayList
来实现的)咱们能够看到CopyOnWriteArrayList
在线程安全的容器里面屡次出现。
首先咱们来看看什么是CopyOnWrite
?Copy-On-Write
简称COW
,是一种用于程序设计中的优化策略。缓存
CopyOnWrite容器即写时复制的容器。通俗的理解是当咱们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,而后新的容器里添加元素,添加完元素以后,再将原容器的引用指向新的容器。这样作的好处是咱们能够对CopyOnWrite容器进行并发的读,而不须要加锁,由于当前容器不会添加任何元素。因此CopyOnWrite容器也是一种读写分离的思想,读和写不一样的容器。
在java里面咱们若是采用不正确的循环姿式去遍历List时候,若是一边遍历一边修改抛出java.util.ConcurrentModificationException
错误的。
若是对ArrayList循环遍历不是很熟悉的能够建议看下这篇文章《ArrayList的删除姿式你都掌握了吗》安全
List<String> list = new ArrayList<>(); list.add("张三"); list.add("java金融"); list.add("javajr.cn"); Iterator<String> iterator = list.iterator(); while(iterator.hasNext()){ String content = iterator.next(); if("张三".equals(content)) { list.remove(content); } }
上面这个栗子是会发生java.util.ConcurrentModificationException
异常的,若是把ArrayList
改成CopyOnWriteArrayList
是不会发生生异常的。多线程
咱们再看下面一个栗子一个线程往List里面添加数据,一个线程循环list读数据。并发
List<String> list = new ArrayList<>(); list.add("张三"); list.add("java金融"); list.add("javajr.cn"); Thread t = new Thread(new Runnable() { int count = 0; @Override public void run() { while (true) { list.add(count++ + ""); } } }); t.start(); Thread.sleep(10000); for (String s : list) { System.out.println(s); }
咱们运行上述代码也会发生ConcurrentModificationException
异常,若是把ArrayList
换成了CopyOnWriteArrayList
就一切正常。app
经过上面两个栗子咱们能够发现CopyOnWriteArrayList
是线程安全的,下面咱们就来一块儿看看CopyOnWriteArrayList
是如何实现线程安全的。dom
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { private static final long serialVersionUID = 8673264195747942595L; /** The lock protecting all mutators */ final transient ReentrantLock lock = new ReentrantLock(); /** The array, accessed only via getArray/setArray. */ private transient volatile Object[] array;
从源码中咱们能够知道CopyOnWriteArrayList
这和ArrayList
底层实现都是经过一个Object
的数组来实现的,只不过 CopyOnWriteArrayList
的数组是经过volatile
来修饰的,为何须要volatile
修饰建议能够看看《Java的synchronized 能防止指令重排序吗?》
还有新增了ReentrantLock
。ide
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
加锁操做来(在jdk11
的时候采用了synchronized
来替换ReentrantLock
)保证多线程写的时候只有一个线程进行数组的复制,不然的话内存中会有多份被复制的数据,致使数据错乱。volatile
修饰的,根据 volatile
的 happens-before
规则,写线程对数组引用的修改是能够当即对读线程是可见的。再Java并发包里提供了两个使用CopyOnWrite
机制实现的并发容器,它们是CopyOnWriteArrayList
和CopyOnWriteArraySet
,可是并无CopyOnWriteHashMap
咱们能够按照他的思路本身来实现一个CopyOnWriteHashMap
public class CopyOnWriteHashMap<K, V> implements Map<K, V>, Cloneable { final transient ReentrantLock lock = new ReentrantLock(); private volatile Map<K, V> map; public CopyOnWriteHashMap() { map = new HashMap<>(); } @Override public V put(K key, V value) { final ReentrantLock lock = this.lock; lock.lock(); try { Map<K, V> newMap = new HashMap<K, V>(map); V val = newMap.put(key, value); map = newMap; return val; } finally { lock.unlock(); } } @Override public V get(Object key) { return map.get(key); } @Override public V remove(Object key) { final ReentrantLock lock = this.lock; lock.lock(); try { Map<K, V> newMap = new HashMap<K, V>(map); if (!newMap.containsKey(key)) { return null; } V v = newMap.get(key); newMap.remove(key); map = newMap; return v; }finally { lock.unlock(); } }
上述咱们实现了一个简单的CopyOnWriteHashMap
,只实现了add、remove、get
方法其余剩余的方法能够自行去实现,涉及到只要数据变化的就要加锁,读无需加锁。
CopyOnWrite
并发容器适用于读多写少的并发场景,好比黑白名单、国家城市等基础数据缓存、系统配置等。这些基本都是只要想项目启动的时候初始化一次,变动频率很是的低。若是这种读多写少的场景采用 Vector,Collections
包装的这些方式是不合理的,由于尽管多个读线程从同一个数据容器中读取数据,可是读线程对数据容器的数据并不会发生发生修改,因此并不须要读也加锁。
CopyOnWriteArrayList虽然是一个线程安全版的ArrayList,但其每次修改数据时都会复制一份数据出来,因此CopyOnWriteArrayList只适用读多写少或无锁读场景。咱们若是在实际业务中使用CopyOnWriteArrayList,必定是由于这个场景适合而非是为了炫技。
由于CopyOnWrite的写时复制机制每次进行写操做的时候都会有两个数组对象的内存,若是这个数组对象占用的内存较大的话,若是频繁的进行写入就会形成频繁的Yong GC和Full GC。
CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。读操做的线程可能不会当即读取到新修改的数据,由于修改操做发生在副本上。但最终修改操做会完成并更新容器因此这是最终一致性。
简单的测试了下CopyOnWriteArrayList 和 Collections.synchronizedList()的读和写发现:
选择CopyOnWriteArrayList的时候必定是读远大于写。若是读写都差很少的话建议选择Collections.synchronizedList。