Java并发编程—写时复制容器

面试官问什么是COW

"靠?我来面试你怎么骂人呢?"。

面试官:"不,不是那个靠。是英文COW。"

"......母牛?"。

面试官汗颜......"你知道写时复制容器么?"

通过这个小例子(非真实存在),加深一下各位对这个简称的印象。

Copy-On-Write简称COW。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。

核心代码

public E set(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();//获得锁
        try {
            Object[] elements = getArray();//得到目前容器数组的一个副本
            E oldValue = get(elements, index);//获得index位置对应元素目前的值

            if (oldValue != element) {
                int len = elements.length;
                //创建一个新的数组newElements,将elements复制过去
                Object[] newElements = Arrays.copyOf(elements, len);
                //将新数组中index位置的元素替换为element
                newElements[index] = element;
                //这一步是关键,作用是将容器中array的引用指向修改之后的数组,即newElements
                setArray(newElements);
            } else {
                //index位置元素的值与element相等,故不对容器数组进行修改
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();//解除锁定
        }
    }

我们可以看到,在set方法中,我们首先是获得了当前数组的一个拷贝获得一个新的数组,然后在这个新的数组上完成我们想要的操作。当操作完成之后,再把原有数组的引用指向新的数组。

这个过程我们发现加了锁。原因数组拷贝的过程并不是一个原子操作,因此需要写写分离。

反观读操作,当写操作完成的那一瞬间,再把原有数组的引用指向新的数组可以视为原子操作。因此任何的读操作都不用加锁,保证读取到的是读那一刻List完整的快照数据。

无论List本身如何变化,迭代器能感知到的都是它在被创建那一刻时List的状态,任何其他线程对List的改变,对本迭代器都不可见。不会出现ConcurrentHashMap的迭代器可能读取到其他线程修改过程中容器的中间状态的情况。由于CopyOnWriteArrayList读操作无法感知最新正在变化的数据,所以和ConcurrentHashMap一样CopyOnWriteArrayList也是弱一致性的。

总结

我们来对比最常见的并发容器ConcurrentHashMap来总结:

  1. ConcurrentHashMap和CopyOnWriteArrayList都是无锁化的读取,所以读操作发生时无法确保目前所有其他线程的写操作已经完成,不可用于要求数据强一致性的场景。
  2. ConcurrentHashMap和CopyOnWriteArrayList都可以保证读取时可以感知到已经完成的写操作。
  3. ConcurrentHashMap读操作可能会感知到同一时刻其他线程对容器写操作的中间状态。CopyOnWriteArrayList永远只会读取到容器在读取时刻的快照状态。
  4. ConcurrentHashMap使用锁分段技术,缩小锁的范围,提高写的并发量。CopyOnWriteArrayList使用写时复制技术,保证并发写入数据时,不会对已经开启的读操作造成干扰。我们不难发现CopyOnWriteArrayList的锁粒度是相当大的,因此并发量过大的场景中,写的效率很差。
  5. ConcurrentHashMap适用于高并发下对数据访问没有强一致性需求的场景。CopyOnWriteArrayList适用于在对并发容器中,元素耦合度较强的场景,可以保证每一个快照中的数据都是一致的。如果当并发容器中的元素过多时,复制的内存成本会过高,且复制效率会进一步下降。

在实际开发中,可以根据业务场景选择并发容器。

猜你喜欢

转载自blog.csdn.net/weixin_47184173/article/details/115269145
今日推荐