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();
}
}
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];
}
2
3
4
5
6
7
# 应用场景
CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景。 使用CopyOnWriteMap需要注意两件事情:
- 减少扩容开销。根据实际需要,初始化CopyOnWriteMap的大小,避免写时CopyOnWriteMap扩容的开销。
- 使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。
缺陷:内存占用问题和数据一致性问题
1、内存占用问题:在进行写操作的时候,内存里会同时驻扎两个对象的内存。
2、数据一致性问题:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
参考资料:聊聊并发-Java中的Copy-On-Write容器 《Java并发编程实战》