ArrayList是线程不安全的,因而JDK新增长了一个线程并发安全的List——CopyOnWriteList,中心思想就是copy-on-write
,简单来讲是读写分离:读时共享、写时复制(本来的array)更新(且为独占式的加锁)
,而咱们下面分析的源码具体实现也是这个思想的体现。java
继承体系:数组
咱们单独看一下CopyOnWriteList的主要属性和下面要主要分析的方法有哪些。从图中看出:安全
每一个CopyOnWriteList对象里面有一个array数组来存放具体元素多线程
使用ReentrantLock独占锁来保证只有写线程对array副本进行更新。并发
CopyOnWriteArrayList在遍历的使用不会抛出ConcurrentModificationException异常,而且遍历的时候就不用额外加锁app
下面仍是主要看CopyOnWriteList的实现ide
//这个就是保证更新数组的时候只有一个线程可以获取lock,而后更新 final transient ReentrantLock lock = new ReentrantLock(); /* 使用volatile修饰的array,保证写线程更新array以后别的线程可以看到更新后的array. 可是并不能保证明时性:在数组副本上添加元素以后,尚未更新array指向新地址以前,别的读线程看到的仍是旧的array */ private transient volatile Object[] array; //获取数组,非private的,final修饰 final Object[] getArray() { return array; } //设置数组 final void setArray(Object[] a) { array = a; }
(1)无参构造,默认建立的是一个长度为0的数组ui
/*这里就是构造方法,建立一个新的长度为0的Object数组 而后调用setArray方法将其设置给CopyOnWriteList的成员变量array*/ public CopyOnWriteArrayList() { setArray(new Object[0]); }
(2)参数为Collection的构造方法this
//按照集合的迭代器遍历返回的顺序,建立包含传入的collection集合的元素的列表 //若是传递的参数为null,会抛出异常 public CopyOnWriteArrayList(Collection<? extends E> c) { Object[] elements; //一个elements数组 //这里是判断传递的是否就是一个CopyOnWriteArrayList集合 if (c.getClass() == CopyOnWriteArrayList.class) //若是是,直接调用getArray方法,得到传入集合的array而后赋值给elements elements = ((CopyOnWriteArrayList<?>)c).getArray(); else { //先将传入的集合转变为数组形式 elements = c.toArray(); //c.toArray()可能不会正确地返回一个 Object[]数组,那么使用Arrays.copyOf()方法 if (elements.getClass() != Object[].class) elements = Arrays.copyOf(elements, elements.length, Object[].class); } //直接调用setArray方法设置array属性 setArray(elements); }
(3)建立一个包含给定数组副本的list线程
public CopyOnWriteArrayList(E[] toCopyIn) { setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class)); }
上面介绍的是CopyOnWriteList的初始化,三个构造方法都比较易懂,后面仍是主要看看几个主要方法的实现
下面是add(E e)方法的实现 ,以及详细注释
public boolean add(E e) { //得到独占锁 final ReentrantLock lock = this.lock; //加锁 lock.lock(); try { //得到list底层的数组array Object[] elements = getArray(); //得到数组长度 int len = elements.length; //拷贝到新数组,新数组长度为len+1 Object[] newElements = Arrays.copyOf(elements, len + 1); //给新数组末尾元素赋值 newElements[len] = e; //用新的数组替换掉原来的数组 setArray(newElements); return true; } finally { lock.unlock();//释放锁 } }
总结一下add方法的执行流程
总结起来就是,多线程下只有一个线程可以获取到锁,而后使用复制原有数组的方式添加元素,以后再将新的数组替换原有的数组,最后释放锁(别的add线程去执行)。
最后还有一点就是,数组长度不是固定的,每次写以后数组长度会+1,因此CopyOnWriteList也没有length或者size这类属性,可是提供了size()方法,获取集合的实际大小,size()方法以下
public int size() { return getArray().length; }
使用get(i)能够获取指定位置i的元素,固然若是元素不存在就会抛出数组越界异常。
public E get(int index) { return get(getArray(), index); } final Object[] getArray() { return array; } private E get(Object[] a, int index) { return (E) a[index]; }
固然get方法这里也体现了copy-on-write-list
的弱一致性问题。咱们用下面的图示简略说明一下。图中给的假设状况是:threadA访问index=1处的元素
由于咱们看到get过程是没有加锁的(假设array中有三个元素如图所示)。假设threadA执行①以后②以前,threadB执行remove(1)操做,threadB或获取独占锁,而后执行写时复制操做,即复制一个新的数组newArray
,而后在newArray中执行remove操做(1),更新array。threadB执行完毕array中index=1的元素已是item3了。
而后threadA继续执行,可是由于threadA操做的是原数组中的元素,这个时候的index=1仍是item2。因此最终现象就是虽然threadB删除了位置为1处的元素,可是threadA仍是访问的原数组的元素。这就是弱一致性问题
修改也是属于写,因此须要获取lock,下面就是set方法的实现
public E set(int index, E element) { //获取锁 final ReentrantLock lock = this.lock; //进行加锁 lock.lock(); try { //获取数组array Object[] elements = getArray(); //获取index位置的元素 E oldValue = get(elements, index); // 要修改的值和原值不相等 if (oldValue != element) { //获取旧数组的长度 int len = elements.length; //复制到一个新数组中 Object[] newElements = Arrays.copyOf(elements, len); //在新数组中设置元素值 newElements[index] = element; //用新数组替换掉原数组 setArray(newElements); } else { // Not quite a no-op; ensures volatile write semantics //为了保证volatile 语义,即便没有修改,也要替换成新的数组 setArray(elements); } return oldValue; //返回旧值 } finally { lock.unlock();//释放锁 } }
看了set方法以后,发现其实和add方法实现相似。
下面是remove方法的实现,总结就是
public E remove(int index) { //获取锁 final ReentrantLock lock = this.lock; //加锁 lock.lock(); try { //获取原数组 Object[] elements = getArray(); //获取原数组长度 int len = elements.length; //获取原数组index处的值 E oldValue = get(elements, index); //由于数组删除元素须要移动,因此这里就是计算须要移动的个数 int numMoved = len - index - 1; //计算的numMoved=0,表示要删除的是最后一个元素, //那么旧直接将原数组的前len-1个复制到新数组中,替换旧数组便可 if (numMoved == 0) setArray(Arrays.copyOf(elements, len - 1)); //要删除的不是最后一个元素 else { //建立一个长度为len-1的数组 Object[] newElements = new Object[len - 1]; //将原数组中index以前的元素复制到新数组 System.arraycopy(elements, 0, newElements, 0, index); //将原数组中index以后的元素复制到新数组 System.arraycopy(elements, index + 1, newElements, index, numMoved); //用新数组替换原数组 setArray(newElements); } return oldValue;//返回旧值 } finally { lock.unlock();//释放锁 } }
迭代器的基本使用方式以下,hashNext()方法用来判断是否还有元素,next方法返回具体的元素。
CopyOnWriteArrayList list = new CopyOnWriteArrayList(); Iterator<?> itr = list.iterator(); while(itr.hashNext()) { //do sth itr.next(); }
那么在CopyOnWriteArrayList中的迭代器是怎样实现的呢,为何说是弱一致性呢(先获取迭代器的,可是若是在获取迭代器以后别的线程对list进行了修改,这对于迭代器是不可见的
),下面就说一下CopyOnWriteArrayList中的实现
//Iterator<?> itr = list.iterator(); public Iterator<E> iterator() { //这里能够看到,是先获取到原数组getArray(),这里记为oldArray //而后调用COWIterator构造器将oldArray做为参数,建立一个迭代器对象 //从下面的COWIterator类中也能看到,其中有一个成员存储的就是oldArray的副本 return new COWIterator<E>(getArray(), 0); } static final class COWIterator<E> implements ListIterator<E> { //array的快照版本 private final Object[] snapshot; //后续调用next返回的元素索引(数组下标) private int cursor; //构造器 private COWIterator(Object[] elements, int initialCursor) { cursor = initialCursor; snapshot = elements; } //变量是否结束:下标小于数组长度 public boolean hasNext() { return cursor < snapshot.length; } //是否有前驱元素 public boolean hasPrevious() { return cursor > 0; } //获取元素 //hasNext()返回true,直接经过cursor记录的下标获取值 //hasNext()返回false,抛出异常 public E next() { if (! hasNext()) throw new NoSuchElementException(); return (E) snapshot[cursor++]; } //other method... }
在上面的代码中咱们能看处,list的iterator()方法实际上返回的是一个COWIterator对象,COWIterator对象的snapshot成员变量保存了当前
list中array存储的内容,可是snapshot能够说是这个array的一个快照,为何这样说呢
咱们传递的是虽然是当前的
array
,可是可能有别的线程对array
进行了修改而后将本来的array
替换掉了,那么这个时候list中的array
和snapshot
引用的array
就不是一个了,做为原array
的快照存在,那么迭代器访问的也就不是更新后的数组了。这就是弱一致性的体现
咱们看下面的例子
public class TestCOW { private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList(); public static void main(String[] args) throws InterruptedException { list.add("item1"); list.add("item2"); list.add("item3"); Thread thread = new Thread() { @Override public void run() { list.set(1, "modify-item1"); list.remove("item2"); } }; //main线程先得到迭代器 Iterator<String> itr = list.iterator(); thread.start();//启动thread线程 thread.join();//这里让main线程等待thread线程执行完,而后再遍历看看输出的结果是否是修改后的结果 while (itr.hasNext()) { System.out.println(Thread.currentThread().getName() + "线程中的list的元素:" + itr.next()); } } }
运行结果以下。实际上再上面的程序中咱们先向list中添加了几个元素,而后再thread中修改list,同时让main线程先得到list的迭代器
,并等待thread执行完而后打印list中的元素,发现 main线程并无发现list中的array的变化,输出的仍是原来的list,这就是弱一致性的体现。
main线程中的list的元素:item1 main线程中的list的元素:item2 main线程中的list的元素:item3
写
时线程安全的:使用ReentrantLock独占锁,保证同时只有一个线程对集合进行写
操做写
操做会更新array) 注意到set方法中有一段代码是这样的
else { //oldValue = element(element是传入的参数) // Not quite a no-op; ensures volatile write semantics //为了保证volatile 语义,即便没有修改,也要替换成新的数组 setArray(elements); }
其实就是说要指定位置要修改的值和数组中那个位置的值是相同的,可是仍是须要调用set方法更新array,这是为何呢,参考这个Why setArray() method call required in CopyOnWriteArrayList,总结就是为了维护happens-before原则。首先看一下这段话
java.util.concurrent 中全部类的方法及其子包扩展了这些对更高级别同步的保证。尤为是: 线程中将一个对象放入任何并发 collection 以前的操做 happen-before 从另外一线程中的 collection 访问或移除该元素的
后续操做
。
能够理解为这里是为了保证set操做以前的系列操做happen-before与别的线程访问array(不加锁)的后续操做
,参照下面的例子
// 这是两个线程的初始状况 int nonVolatileField = 0; //一个不被volatile修饰的变量 //伪代码 CopyOnWriteArrayList<String> list = {"x","y","z"} // Thread 1 // (1)这里更新了nonVolatileField nonVolatileField = 1; // (2)这里是set()修改(写)操做,注意这里会对volatile修饰的array进行写操做 list.set(0, "x"); // Thread 2 // (3)这里是访问(读)操做 String s = list.get(0); // (4)使用nonVolatileField if (s == "x") { int localVar = nonVolatileField; }
假设存在以上场景,若是能保证只会存在这样的轨迹:(1)->(2)->(3)->(4).根据上述java API文档中的约定有
(2)happen-before与(3),在线程内的操做有(1)happen-before与(2),(3)happen-before与(4),根据happen-before的传递性读写nonVolatileField变量就有(1)happen-before与(4)
因此Thread 1对nonVolatileField的写操做对Thread 2中a的读操做可见。若是CopyOnWriteArrayList的set的else里没有setArray(elements)对volatile变量的写
的话,(2)happen-before与(3)就再也不有了,上述的可见性也就没法保证。因此就是为了保证set操做以前的系列操做happen-before与别的线程访问array(不加锁)的后续操做