CopyOnWriteArrayList类
CopyOnWriteArrayList是一个线程安全的ArrayList,对其修改操作都是在底层的一个复制的数组上进行的,也就是使用了写时复制策略。
一、变量与构造方法
/** The lock protecting all mutators */ final transient ReentrantLock lock = new ReentrantLock();//独占锁,保证同一时刻只有一个线程对array修改 /** The array, accessed only via getArray/setArray. */ private transient volatile Object[] array;//存放对象的数组 public CopyOnWriteArrayList() {//无参实例化 array = new Object[0]; setArray(new Object[0]); } /** * Collection接口实例创建实例化 将Collection的array复制给CopyOnWriteArrayList的array*/ public CopyOnWriteArrayList(Collection<? extends E> c) { Object[] elements; if (c.getClass() == CopyOnWriteArrayList.class) elements = ((CopyOnWriteArrayList<?>)c).getArray(); else { elements = c.toArray(); // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elements.getClass() != Object[].class) elements = Arrays.copyOf(elements, elements.length, Object[].class); } setArray(elements); } /** * 直接数组实例化 复制给array*/ public CopyOnWriteArrayList(E[] toCopyIn) { setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class)); }
二、写时复制策略:增删改时复制数组,修改后替换原数组。
1.boolean add(E e)方法
public boolean add(E e) { //获取独占锁 final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; //复制array到新数组,添加元素到新数组 Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; //新数组替换原数组 setArray(newElements); return true; } finally { lock.unlock(); } }
2.E remove(int index)
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 { //创建新数组,分两次复制删除后的剩余的元素到新数组 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(); } }
3.E set(int index, E element):
public E set(int index, E element) { //获取独占锁 final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); 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(); } }
三、弱一致性问题,读操作时不能满足实时性要求
弱一致性问题是由写时复制策略产生的,读写操作并发时,读操作时数组的版本可能是写操作版本的原数组,不是替换的新数组。不能保证实时性要求。
1.E get(int index):未加锁,弱一致性问题:remove()、set()的同时执行get()方法,可能会导致获取已删除、修改前的元素。
public E get(int index) { return get(getArray(), index); } private E get(Object[] a, int index) { return (E) a[index]; }
2.迭代器iterator的弱一致性fast-safe机制,返回迭代器后,遍历的是返回迭代器时数组的一个快照。其他线程对list的增删改对迭代器是不可见的。
快照版本,指针指向的是增删改前的数组,而不是增删改后的新数组,操作的是两个不同的数组
为什么写操作加锁了已经能保证线程安全了,还要复制数组?这里可能就是答案吧,读操作有get和iterator,复制数组是为了实现fast-safe机制,避免fast-fail。
public Iterator<E> iterator() { return new COWIterator<E>(getArray(), 0); } static final class COWIterator<E> implements ListIterator<E> { /** Snapshot of the array */ private final Object[] snapshot; /** Index of element to be returned by subsequent call to next. */ private int cursor; private COWIterator(Object[] elements, int initialCursor) { cursor = initialCursor; snapshot = elements;//快照版本,指针指向的是增删改前的数组,而不是增删改后的新数组,操作的是两个不同的数组 } ... }
例子:
public class COWTest { private static volatile CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>(); public static void main(String[] args) throws InterruptedException { cowList.add("hello"); cowList.add("world"); cowList.add("skip1"); cowList.add("skip2"); Thread threadOne = new Thread(new Runnable() { @Override public void run() { cowList.set(0,"hello "); cowList.remove(3); cowList.remove(2); } }); Iterator<String> iterator = cowList.iterator(); threadOne.start(); threadOne.join(); while (iterator.hasNext()){ System.out.println(iterator.next());//hello\nworld\nskip1\nskip2\n } System.out.println(cowList);//[hello , world] } }
总结:
1.写时复制策略,写时加锁了已经可以保证线程安全了,复制是为了避免fast-fail机制,实现fast-safe,每次增删改都会复制生成一个新数组,当数组太大时,每次写操作代价太大,性能很低,另外JVM规定大对象会直接进入老年代,可能导致full gc
2.弱一致性,虽然能做到最终一致性,但是不能保证实时性需求
3.将读写分开处理,适合读多写少的场景。
4.扩容与ArrayList不同,add时+1,remove时-1
4.增删改共用同一个独占锁,所以操作互斥。
参考自《java并发编程之美》