在解决并发性读取的问题上,有一种模式叫做Copy On Write,基本思想就是以空间换时间。Mysql中RethinkDB的B-tree索引数据结构就是采用copy-on-write,Java类中的CopyOnWriteList也是对应模式的List实现。这里讲解一下常用的CopyOnWriteList,学习其思想,可以应用到类似的场景。 传统的ArrayList是线程不安全的,其对应线程安全版本Vector对所有操作加锁,并发损耗太大。因此出现了CopyOnWrite这种妥协方案,保证了多线程环境下的读写安全。
CopyOnWrite模式的思想:可变操作(add、set、remove等)都是通过对底层数组进行一次新的复制来实现。需要很大的开销,因此其应用场景是当遍历操作的数量大大超过其可变操作的数量。
CopyOnWriteList返回的是COWIterator迭代器,通过源码可以看出,迭代器不支持修改操作
- public void remove() {
- throw new UnsupportedOperationException();
- }
- public void set(E e) {
- throw new UnsupportedOperationException();
- }
- public void add(E e) {
- throw new UnsupportedOperationException();
- }
该迭代器构造的时候得到的是当前数组的一个拷贝,源码如下
- public ListIterator<E> listIterator() {
- return new COWIterator<E>(getArray(), 0);
- }
- private static class COWIterator<E> implements ListIterator<E> {
- /** Snapshot of the array **/
- private final Object[] snapshot;
CopyOnWriteList每进行一次可变操作,就会将原来的数组复制到新的数组上,将当前数组指向新的数组。add的源码如下:
- 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();
- }
- }
为什么要进行整体拷贝呢,不能像原来的ArrayList那样进行扩容吗?线程A获取迭代器进行迭代得到当前的数组拷贝,此时线程B进行add操作,加入元素。如果不进行拷贝,在新的数组上进行操作,还是在原有数组加入元素。那么线程A迭代得到的数组就会被改变,那么遍历结果就是错乱的。进行了拷贝之后,线程A遍历的还是当初它要求的数组,没有改变,此时List指向的是新的改变后的数组,提供给新的线程进行迭代。如果不进行拷贝,那么就必须在迭代过程中加锁,禁止数组改变。