CopyOnWrite

Copy-On-Write容器

概念

Copy-On-Write简称COW,即写时复制,是一种程序设计中的优化策略。其基本思路是:从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。

当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器,体现的是一种以空间换时间的思想。

//add的时候,使用重入锁,保证只有一个线程在修改
public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //获取当前数组
            Object[] elements = getArray();
            int len = elements.length;
            //copy当前数组到newElements
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //将元素添加到newElements
            newElements[len] = e;
            //将集合中的底层数组修改为指向newElements
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
}
//get并未使用任何锁,所以在一个线程add的同时,其他线程均可以进行get操作
public E get(int index) {
        return get(getArray(), index);
}

适用场景

读多写少,比如黑名单,白名单,商品类目的访问和更新等。

问题

  • 内存占用大:在进行写操作的时候,内存里会同时驻扎两个对象的内存。如果这些对象占用的内存比较大,很有可能造成频繁的Yong GC和Full GC。针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。

  • 数据只能保证最终一致性,非实时一致性:所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

Copy-On-Write实现类

CopyOnWriteArrayList

  • java.util.ArrayList的线程安全版本:所有的修改操作都是通过对底层数组的最新copy来实现。

  • 允许null值:All elements are permitted, including null

CopyOnWriteArraySet

  • 所有操作在内部使用CopyOnWriteArrayListjava.util.Set

  • set元素数量少,只读操作远多于修改操作,在遍历的时候需要排除其他线程的干扰。

无论是CopyOnWriteArrayList还是CopyOnWriteArraySet,由于都是采用的写时复制的思想,所以要注意以下几点:

  • 只适合读多写少的场景,因为修改操作(addsetremove等)的代价非常昂贵:需要对整个底层数组进行copy。

  • 关于迭代操作需要注意的地方

    • 迭代方法使用的是迭代开始时的数组的引用,这个数组在该迭代器迭代的整个过程中都不会改变,所以该接口和迭代器不会抛出ConcurrentModificationException

    • 迭代器不会反应出该迭代器创建之后对应list的所有增加、移除、修改等操作。

    • 迭代器自身的元素改变方法,如removesetadd都是不支持的。

参考

JAVA中的COPYONWRITE容器

Last updated