读多写少下场景下,快照避免读写冲突
原理
多线程下读写冲突
一个线程遍历过程中,很可能其他线程有更改需求,遍历时长不固定,完全取决于外部逻辑控制,修改需求一直等待不现实
一个线程写数据,多个读线程都要等待
解决方案是任何修改都会产生一个全新副本,重量级操作,适合读多写少场景
当一个线程遍历集合时,集合实际数据为A,遍历迭代器基于数据A引用
遍历过程中另一线程发起修改,数据A拷贝生成数据B,集合实际数据被替换为B
虽然集合数据变化,但是遍历迭代器依然基于数据A,即遍历无视后续集合变化,以发起遍历时刻为准
遍历过程中可能同时存在多个副本,结束后数据A被回收,集合持有数据B,集合永远持有最新的一份
CopyOnWriteArrayList
底层是一个普通数组外加一个重入锁
CopyOnWriteArrayList.java 1 2 3 4 5 6 7 public class CopyOnWriteArrayList <E > implements List <E >, RandomAccess , Cloneable , java .io .Serializable { final transient ReentrantLock lock = new ReentrantLock(); private transient volatile Object[] array; }
尾部增加比较直观,先上锁,分配新数组容量,完整拷贝后新值设到末尾
CopyOnWriteArrayList.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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(); } }
指定位置插入稍微复杂一点,位置前后两段拷贝,再加上新元素拼合
CopyOnWriteArrayList.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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(); } }
Set操作虽然不会造成数组容量变化,但是依然整体拷贝后替换
CopyOnWriteArrayList.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 { setArray(elements); } return oldValue; } finally { lock.unlock(); } }
遍历使用的迭代器是基于创建时的底层数组,不受修改影响
CopyOnWriteArrayList.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public Iterator<E> iterator () { return new COWIterator<E>(getArray(), 0 ); } static final class COWIterator <E > implements ListIterator <E > { private final Object[] snapshot; private int cursor; private COWIterator (Object[] elements, int initialCursor) { cursor = initialCursor; snapshot = elements; } public boolean hasNext () { return cursor < snapshot.length; } @SuppressWarnings ("unchecked" ) public E next () { if (! hasNext()) throw new NoSuchElementException(); return (E) snapshot[cursor++]; } public void remove () { throw new UnsupportedOperationException(); } public void set (E e) { throw new UnsupportedOperationException(); } public void add (E e) { throw new UnsupportedOperationException(); } }
普通读操作没有锁保护,因为array引用可能随时被替换,读不是直接在底层array上操作,而是先取array引用即拿到当前数组,后续再替换也无关,相当于一个固定操作
CopyOnWriteArrayList.java 1 2 3 public E get (int index) { return get(getArray(), index); }
CopyOnWriteArraySet
完全基于CopyOnWriteArrayList实现,不同的是添加时保持无重复
CopyOnWriteArraySet.java 1 2 3 4 5 6 7 8 9 10 11 12 public class CopyOnWriteArraySet <E > extends AbstractSet <E > implements java .io .Serializable { private final CopyOnWriteArrayList<E> al; public CopyOnWriteArraySet () { al = new CopyOnWriteArrayList<E>(); } public boolean add (E e) { return al.addIfAbsent(e); } }