我不停奔跑只为追赶当年被寄予厚望的自己。 ——利文斯顿
我们知道 ArrayList 非线程安全,需要自己加锁或者使用 Collections.synchronizedList
包装. 从JDK1.5开始JUC里提供了使用 CopyOnWrite 机制实现的并发容器线程安全的 List - CopyOnWriteArrayList,简称 COW
CopyOnWrite 写时复制. 一般来说就是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器复制出一个新的容器,往新的容器里添加元素,添加完元素之后,再将原容器引用指向新容器. 即一开始大家都在共享同一内容,当有人想修改该内容时,才会真地把内容copy出去形成一个新的内容然后再改,这是一种延时懒惰策略.
可并发读 CopyOnWrite 容器,而无需加锁,因为当前容器不会添加任何元素. 所以这也是一种读写分离的思想,读写的是不同的容器.
下面开始看源码,到底是如何实现写时复制的.
向 COW 里添加元素,是需要加锁的,否则并发写时 copy 出N个副本!
public boolean add(E e) { final ReentrantLock lock = this.lock; // 1.加锁 lock.lock(); try { // 得到原数组 Object[] elements = getArray(); int len = elements.length; // 2.复制出新数组,加一是因为要添加yi'ge'yuan's Object[] newElements = Arrays.copyOf(elements, len + 1); // 把新元素添加到新数组里,直接放在数组尾部 newElements[len] = e; // 把原数组引用指向新数组 setArray(newElements); return true; } finally { // finally 里面释放锁,保证即使 try 发生了异常,仍然能够释放锁 lock.unlock(); } } 复制代码
读时无需加锁,如果读时其它线程正在向ArrayList添加数据,读还是只会读到旧数据,因为写时并不会锁住旧的数组.
public E remove(int index) { final ReentrantLock lock = this.lock; // 加锁 lock.lock(); try { Object[] elements = getArray(); int len = elements.length; // 先得到旧值 E oldValue = get(elements, index); int numMoved = len - index - 1; // 如果要删除的数据正好是数组的尾部,直接删除 if (numMoved == 0) setArray(Arrays.copyOf(elements, len - 1)); else { // 若删除的数据在数组中间: // 1. 设置新数组的长度减一,因为是减少一个元素 // 2. 从 0 拷贝到数组新位置 // 3. 从新位置拷贝到数组尾部 Object[] newElements = new Object[len - 1]; System.arraycopy(elements, 0, newElements, 0, index); System.arraycopy(elements, index + 1, newElements, index, numMoved); setArray(newElements); } return oldValue; } finally { lock.unlock(); } } 复制代码
依旧三板斧:
public boolean removeAll(Collection<?> c) { if (c == null) throw new NullPointerException(); final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; if (len != 0) { // newlen 表新数组的索引位置,新数组中存在不包含在 c 中的元素 int newlen = 0; Object[] temp = new Object[len]; // 循环,把不包含在 c 里面的元素,放到新数组中 for (int i = 0; i < len; ++i) { Object element = elements[i]; // 不包含在 c 中的元素,从 0 开始放到新数组中 if (!c.contains(element)) temp[newlen++] = element; } // 拷贝新数组,变相的删除了不包含在 c 中的元素 if (newlen != len) { setArray(Arrays.copyOf(temp, newlen)); return true; } } return false; } finally { lock.unlock(); } } 复制代码
并非直接对数组元素逐个删除,而先对数组值循环判断,将无需删除的数据放到临时数组,最后临时数组中的数据就是我们不需要删除的数据.
CopyOnWrite 并发容器适用于读多写少的并发场景.CopyOnWrite容器有很多优点,但同时也存在问题,开发时候需要注意:
写时,内存里会同时驻存两个对象的内存,旧对象和新写入对象(复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存).若这些对象占用内存较大,很可能造成频繁GC,应用响应时间也变长. 针对该问题,可通过压缩容器中元素,减少大对象的内存,或者直接不使用CopyOnWrite容器,而使用其他并发容器,如ConcurrentHashMap。
CopyOnWrite容器只能保证数据的 最终一致性
,不能保证数据的 实时一致性
,请酌情使用.