在这篇文章中来说下Java并发容器中的CopyOnWriteArrayList。
CopyOnWriteArrayList是线程安全的。从名称上就可以看出来,它采用了的是写时复制的方法,简单来说,就是在对数组列表进行更新时先将原数组拷贝一份新数组,然后在新数组上进行操作,最后将原数组elements的引用指向新数组。
什么是写时复制(Copy-On-Write,简称COW)呢?写时复制,是程序设计中的一种优化策略,其基本思路就是从一开始大家都是共享一块内容,但是如果有人想修改,那么就重新拷贝一份新的,并在新的内容上进行修改,这是一种延时懒惰策略。从JDK1.5开始,提供了两个基于Copy-On-Write的容器,一个是CopyOnWriteArrayList,另一个是CopyOnWriteArraySet。
下面来看一下往这个容器中添加新元素时内部是如何实现的。(以JDK1.7源码为参考)
/** * Appends the specified element to the end of this list. * * @param e element to be appended to this list * @return <tt>true</tt> (as specified by {@link Collection#add}) */ 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(int index, E element)就是在指定的index索引处添加指定的元素,如下所示:
/** * Inserts the specified element at the specified position in this * list. Shifts the element currently at that position (if any) and * any subsequent elements to the right (adds one to their indices). * * @throws IndexOutOfBoundsException {@inheritDoc} */ public void add(int index, E element) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; if (index > len || index < 0) throw new IndexOutOfBoundsException("Index: "+index+ ", Size: "+len); Object[] newElements; int numMoved = len - index; if (numMoved == 0) newElements = Arrays.copyOf(elements, len + 1); else { newElements = new Object[len + 1]; System.arraycopy(elements, 0, newElements, 0, index); System.arraycopy(elements, index, newElements, index + 1, numMoved); } newElements[index] = element; setArray(newElements); } finally { lock.unlock(); } }
这个方法可以看出,添加元素时index不是可以随便传的,必须小于等于当前数组中元素的个数,即index<=length(elements),否则将会排除异常。如果索引index有效,那么如果插入位置是之前元素数组的末尾,则只需要创建一个新数组然后赋值最后一个元素为添加元素即可,如果插入位置<length(elements),那么就牵涉到前面元素的位置迁移,这里使用System.arraycopy进行数组元素的拷贝。
由于所有的写操作都是在最新数组的基础上进行的,这时候如果有并发的写,就需要加锁进行控制,但是对于并发的读呢,这种情况下细分为如下几种场景:
(a)如果写操作未完成,那么直接读取原数组的数据;
(b)如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
(c)如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据;
从以上几种场景的分析结果来看,并发读的情况下是不需要加锁控制的。
经过上面的分析,CopyOnWriteArrayList虽然解决了并发情况下使用ArrayList时非线程安全的问题,但是由于采用的是写时复制的方法,因此在高并发情况下写数据存在一定的性能问题。
感谢大家的阅读,如果有对Java编程、中间件、数据库、及各种开源框架感兴趣,欢迎关注我的博客和头条号(源码帝国),博客和头条号后期将定期提供一些相关技术文章供大家一起讨论学习,谢谢。
如果觉得文章对您有帮助,欢迎给我打赏,一毛不嫌少,一百不嫌多,^_^谢谢。