CopyOnWriteArrayList原理分析介绍Java并发包中的concurrentList只有CopyOnWriteArrayList,CopyOnWriteArrayList是一个线程安全的ArrayList,对其的修改操作是在底层复制的数组(快照)上进行的是的,即使用了copy-on-write策略。在CopyOnWriteArrayList的类图中,每个CopyOnWriteArrayList对象都有一个array数组来存放具体的元素,ReentrantLock独占锁保证只有一个线程同时修改数组。如果我们要制作一个写时复制线程安全列表,我们会怎么做,我们应该考虑哪些要点?什么时候初始化列表,初始化列表元素的数量是多少,列表是有限大小的吗?如何保证线程安全,比如多线程读写时如何保证线程安全?使用迭代器遍历列表时如何保证数据的一致性?我们来看看CopyOnWriteArrayList是如何实现的。main方法解析初始化在无参构造函数中,默认创建一个大小为0的Object数组作为初始值。publicCopyOnWriteArrayList(){setArray(newObject[0]);}参数化构造函数://传入的toCopyIn副本publicCopyOnWriteArrayList(E[]toCopyIn){setArray(Arrays.copyOf(toCopyIn,toCopyIn.length,Object[].class));}//入参为集合,复制到列表中publicCopyOnWriteArrayList(Collectionc){Object[]elements;如果(c.getClass()==CopyOnWriteArrayList.class)元素=((CopyOnWriteArrayList>)c).getArray();else{元素=c.toArray();//c.toArray可能(错误地)不返回Object[](参见6260652)if(elements.getClass()!=Object[].class)elements=Arrays.copyOf(elements,elements.length,Object[].class);}setArray(elements);}添加元素CopyOnWriteArrayList中用来添加元素的函数有:add(Ee)add(intindex,Ee)addIfAbsent(Ee)addAllAbsent(Collectionc)等函数有相似的原理。下面以add(Ee)为例进行分析。publicbooleanadd(Ee){//获取独占锁finalReentrantLocklock=this.lock;锁.锁();try{//获取数组Object[]elements=getArray();intlen=elements.length;//将数组复制到新数组并向新数组添加新元素Object[]newElements=Arrays.copyOf(elements,len+1);新元素[len]=e;//用新数组替换旧数组setArray(newElements);返回真;}finally{//释放独占锁lock.unlock();}}在上面的代码中,排他锁会先获得,如果多个线程同时调用add方法,只有一个线程可以获得锁,其他线程会被阻塞,直到锁被释放。然后用新的数组替换原来的数组,释放锁。需要注意的是,添加元素时,先复制一个快照,然后添加到快照中,而不是直接添加到原始数组中。获取指定位置的元素,使用get(intindex)方法获取下标为index的元素。如果该元素不存在,将抛出IndexOutOfBoundsException。publicEget(intindex){returnget(getArray(),index);}finalObject[]getArray(){returnarray;}privateEget(Object[]a,intindex){return(E)a[index];}上述代码中,当线程调用get方法获取指定位置的元素时,先获取array数组,然后通过下标获取指定位置的元素。这是一个两步操作,但是整个过程没有锁Synchronize。假设数组中有元素1、2、3。由于第一步获取数组和第二步根据下标访问指定位置的元素没有加锁,这可能会导致线程x在第一步之后第二步之前执行remove操作,假设1被删除,remove操作先获取排他锁并进行写时复制,即复制当前数组,然后在复制的数组中删除线程x通过get方法访问的元素1,然后使数组指向新数组。此时array指向的数组的引用计数为1而不是0,因为线程x还在使用它。这时线程x开始执行第二步,操作的数组是线程y删除元素前的数组。总结:虽然线程y删除了index处的元素,但是线程x的第二步还是会返回index处的元素。这其实是copy-on-write策略导致的弱一致性问题。修改指定元素使用set(intindex,Eelement)修改列表中指定元素的值,如果指定元素的元素不存在,将抛出IndexOutOfBoundsException。publicEset(intindex,Eelement){finalReentrantLocklock=this.lock;锁.锁();尝试{对象[]元素=getArray();EoldValue=get(元素,索引);if(oldValue!=element){intlen=elements.length;对象[]newElements=Arrays.copyOf(元素,len);新元素[索引]=元素;设置数组(新元素);}else{//不完全是空操作;确保易失性写入语义setArray(elements);}返回旧值;}最后{lock.unlock();}}这个方法也是先获取排它锁,然后获取当前数组,调用get方法指定位置元素。如果指定的位置元素不等于新值,则创建一个新数组并将元素复制到新数组中。如果指定位置的元素与新值相同,为了保证volatile语义,数组仍然需要重新设置,虽然数组的内容没有改变。目的是刷新缓存并通知其他线程,也就是所谓的可见的操作结果。删除元素删除列表中的指定元素,可以使用下面的方法。Eremove(intindex)booleanremove(Objecto)Booleanremove(Objecto,Object[]snapshot,intindex)等原理大致类似,这里对remove(intindex)方法进行说明。publicEremove(intindex){//获取独占锁finalReentrantLocklock=this.lock;锁.锁();尝试{对象[]元素=getArray();intlen=elements.length;EoldValue=get(元素,索引);intnumMoved=len-索引-1;//如果最后一个元素被删除if(numMoved==0)setArray(Arrays.copyOf(elements,len-1));else{//点两次删除后的剩余元素复制到新数组Object[]newElements=newObject[len-1];System.arraycopy(元素,0,newElements,0,索引);System.arraycopy(元素,索引+1,newElements,索引,numMoved);设置数组(新元素);}返回旧值;}最后{lock.unlock();}}先获取排它锁,保证数据删除时其他线程无法修改数组,然后获取数组中待删除的元素,并将剩余元素复制到新数组中,然后用新数组替换原数组,最后在返回前释放锁。迭代器我们看看CopyOnWriteArrayList中迭代器的弱一致性是什么。所谓弱一致性,就是迭代器返回后,其他线程对链表的增删改查,迭代器是不可见的。让我们看看这是如何做到的。publicIterator
