Copy-On-Write是并发编程的一种优化策略, 其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是
CopyOnWriteArrayList
和CopyOnWriteArraySet
。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。
什么是Copy-On-Write容器?
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。
所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
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出多个副本
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();
}
}
获取元素操作
// 获取元素时没有加锁,这里可以比使用同步提升一定的效率,但是会读出旧数据
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
Copy-On-Write的原理就是这些,我们可以很轻松的实现一个类似的Copy-On-Write容器
public class CopyOnWriteMap<K, V> implements Map<K, V>, Cloneable {
private volatile Map<K, V> internalMap;
public CopyOnWriteMap() {
internalMap = new HashMap<K, V>();
}
public V put(K key, V value) {
synchronized (this) {
Map<K, V> newMap = new HashMap<K, V>(internalMap);
V val = newMap.put(key, value);
internalMap = newMap;
return val;
}
}
public V get(Object key) {
return internalMap.get(key);
}
public void putAll(Map<? extends K, ? extends V> newData) {
synchronized (this) {
Map<K, V> newMap = new HashMap<K, V>(internalMap);
newMap.putAll(newData);
internalMap = newMap;
}
}
}
CopyOnWrite的使用场景
CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存。发生修改时候做copy,新老版本分离,保证读的高性能,适用于以读为主的情况。
需注意:
减少扩容开销。根据实际需要,初始化CopyOnWrite的容器大小,避免写时CopyOnWrite容器扩容的开销。
使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。
CopyOnWrite的缺点
内存占用问题,频繁的复制数组会需要消耗大量的内存空间.
数据一致性问题,Copy-On-Wirte是使用读优先策略,减少的同步损耗,所以会产生数据一致性问题.
针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。