Java高并发编程之并发容器 CopyOnWrite

# CopyOnWrite(写时复制)

抽象概念的CopyOnWrite机制(读写分离)有两个实现类,CopyOnWriteArrayList 与 CopyOnWriteArraySet。CopyonwriteArrayLlst是同步List的一个并发替代品,提供了更好的并发性,并避免了在迭代期间对容器加锁和复制。(相似地,CopyOnWriteArraySet是同步set的一个并发替代品。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,添加或修改完元素之后,再将原容器的引用指向新的容器,这是一种延时懒惰策略。

“写入时复制( copy-on-write)”容器的线程安全性来源于:只要有效的不可变对象被正确发布,那么访问它将不再需要更多的同步。在每次需要修改时,会创建并重新发布一个新的容器拷贝,以此来实现可变性。“写入时复制( copy-on-write)”容器的迭代器保留一个底层基础数组( the backing array)的引用。这个歌数组作为迭代器的起点,永远不会被修改。

# CopyOnWriteArrayList的实现原理

/**
    * Appends the specified element to the end of this list.
    * @param e element to be appended to this list
    * @return {@code true} (as specified by {@link Collection#add})
    */
public boolean add(E e) {
    final ReentrantLock lock = this.lock;  // 加锁,否则多线程写的时候会Copy出N个副本。
    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();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

读的时候不需要加锁,如果读的时候有多个线程正在向ArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的ArrayList。

public E get(int index) {
    return get(getArray(), index);
}

private E get(Object[] a, int index) {
    return (E) a[index];
}
1
2
3
4
5
6
7

# 应用场景

CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景。 使用CopyOnWriteMap需要注意两件事情:

  • 减少扩容开销。根据实际需要,初始化CopyOnWriteMap的大小,避免写时CopyOnWriteMap扩容的开销。
  • 使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。

缺陷:内存占用问题和数据一致性问题

1、内存占用问题:在进行写操作的时候,内存里会同时驻扎两个对象的内存。

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

参考资料:聊聊并发-Java中的Copy-On-Write容器 《Java并发编程实战》

上次更新: 2020/07/29, 14:07:00
最近更新
01
RabbitMQ简介
10-27
02
聊聊Java多态
10-21
03
JVM垃圾回收器
10-16
更多文章>