CopyOnWrite详解-黑马深入学习Java并发编程笔记
CopyOnWrite
原理分析
CopyOnWriteArrayList 采用了写入时拷贝的思想,增删改操作会将底层数组拷贝一份,在新数组上执行操作,不影响其它线程的并发读,读写分离
CopyOnWriteArraySet 底层对 CopyOnWriteArrayList 进行了包装,装饰器模式
public CopyOnWriteArraySet() { al = new CopyOnWriteArrayList<E>(); }
- 存储结构:
private transient volatile Object[] array; // volatile 保证了读写线程之间的可见性
- 全局锁:保证线程的执行安全
final transient ReentrantLock lock = new ReentrantLock();
- 新增数据:需要加锁,创建新的数组操作
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(); } }
- 读操作:不加锁,在原数组上操作
public E get(int index) { return get(getArray(), index); } private E get(Object[] a, int index) { return (E) a[index]; }
适合读多写少的应用场景
- 迭代器:CopyOnWriteArrayList 在返回迭代器时,创建一个内部数组当前的快照(引用),即使其他线程替换了原始数组,迭代器遍历的快照依然引用的是创建快照时的数组,所以这种实现方式也存在一定的数据延迟性,对其他线程并行添加的数据不可见
public Iterator<E> iterator() { // 获取到数组引用,整个遍历的过程该数组都不会变,一直引用的都是老数组, return new COWIterator<E>(getArray(), 0); } // 迭代器会创建一个底层array的快照,故主类的修改不影响该快照 static final class COWIterator<E> implements ListIterator<E> { // 内部数组快照 private final Object[] snapshot; private COWIterator(Object[] elements, int initialCursor) { cursor = initialCursor; // 数组的引用在迭代过程不会改变 snapshot = elements; } // 【不支持写操作】,因为是在快照上操作,无法同步回去 public void remove() { throw new UnsupportedOperationException(); } }