在解决并发性读取的问题上,有一种模式叫做Copy On Write,基本思想就是以空间换时间。Mysql中RethinkDB的B-tree索引数据结构就是采用copy-on-write,Java类中的CopyOnWriteList也是对应模式的List实现。这里讲解一下常用的CopyOnWriteList,学习其思想,可以应用到类似的场景。 传统的ArrayList是线程不安全的,其对应线程安全版本Vector对所有操作加锁,并发损耗太大。因此出现了CopyOnWrite这种妥协方案,保证了多线程环境下的读写安全。

CopyOnWrite模式的思想:可变操作(add、set、remove等)都是通过对底层数组进行一次新的复制来实现。需要很大的开销,因此其应用场景是当遍历操作的数量大大超过其可变操作的数量。

 

    CopyOnWriteList返回的是COWIterator迭代器,通过源码可以看出,迭代器不支持修改操作

  1. public void remove() { 
  2.     throw new UnsupportedOperationException(); 
  3.  
  4. public void set(E e) { 
  5.     throw new UnsupportedOperationException(); 
  6.  
  7.  
  8. public void add(E e) { 
  9.     throw new UnsupportedOperationException(); 

    该迭代器构造的时候得到的是当前数组的一个拷贝,源码如下

  1. public ListIterator<E> listIterator() { 
  2.        return new COWIterator<E>(getArray(), 0); 
  3.    } 
  4.  
  5.  
  6. private static class COWIterator<E> implements ListIterator<E> { 
  7.        /** Snapshot of the array **/ 
  8.        private final Object[] snapshot; 

    CopyOnWriteList每进行一次可变操作,就会将原来的数组复制到新的数组上,将当前数组指向新的数组。add的源码如下:

  1. public boolean add(E e) { 
  2.     final ReentrantLock lock = this.lock;//得到的是当前对象的锁 
  3.     lock.lock(); 
  4.     try { 
  5.         Object[] elements = getArray(); 
  6.         int len = elements.length; 
  7.         Object[] newElements = Arrays.copyOf(elements, len + 1);//整体拷贝 
  8.         newElements[len] = e; 
  9.         setArray(newElements);//迁移到新的数组 
  10.         return true
  11.     } finally { 
  12.         lock.unlock(); 
  13.     } 
  14.     } 

    为什么要进行整体拷贝呢,不能像原来的ArrayList那样进行扩容吗?线程A获取迭代器进行迭代得到当前的数组拷贝,此时线程B进行add操作,加入元素。如果不进行拷贝,在新的数组上进行操作,还是在原有数组加入元素。那么线程A迭代得到的数组就会被改变,那么遍历结果就是错乱的。进行了拷贝之后,线程A遍历的还是当初它要求的数组,没有改变,此时List指向的是新的改变后的数组,提供给新的线程进行迭代。如果不进行拷贝,那么就必须在迭代过程中加锁,禁止数组改变。