CopyOnWrite(写时复制)是一种并发容器的实现思想,主要应用于以下两个类中:
CopyOnWriteArrayListCopyOnWriteArraySet
其核心理念是:
当有线程要修改容器时,不直接操作原容器,而是先复制一份副本,在副本上进行修改,待修改完成后再用新副本替换旧容器的引用。
这意味着:
- 读操作无需加锁:读线程始终访问的是稳定的旧副本,因此不会受到写操作的影响;
- 写操作成本较高:每次写入都需要复制整个底层数组,以保证线程安全。
CopyOnWriteArrayList#
与 ArrayList 类似,CopyOnWriteArrayList 的核心数据结构同样是一个数组,同时额外使用一个 ReentrantLock 来保护写操作。
private transient volatile Object[] array;
final transient ReentrantLock lock = new ReentrantLock();java读操作#
常见的读操作如下:
final Object[] getArray() {
return array;
}
public E get(int index) {
return get(getArray(), index);
}
public boolean contains(Object o) {
Object[] elements = getArray();
return indexOf(o, elements, 0, elements.length) >= 0;
}
public int indexOf(E e, int index) {
Object[] elements = getArray();
return indexOf(e, elements, index, elements.length);
}java可以看到,所有读操作都没有加锁。
那么,它是如何保证线程安全的呢?
关键在于写操作的实现:读线程总是读取当前的数组副本,而写线程在更新时会创建新的副本,因此读写不会互相干扰。
写操作#
以 add(E e) 方法为例:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
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();
}
}
final void setArray(Object[] a) {
array = a;
}java写操作流程如下:
- 获取独占锁;
- 复制原数组;
- 在新数组上执行写入;
- 将新数组替换为当前数组引用;
- 释放锁。
其他写操作(如 remove()、set())的实现逻辑也类似。
CopyOnWriteArraySet#
CopyOnWriteArraySet 是基于数组实现的 Set,其内部实际封装了一个 CopyOnWriteArrayList:
private final CopyOnWriteArrayList<E> al;
/**
* Creates an empty set.
*/
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}java区别在于,CopyOnWriteArraySet 会确保元素不重复。
添加元素时,会先检查元素是否已存在:
public boolean add(E e) {
return al.addIfAbsent(e);
}
public boolean addIfAbsent(E e) {
Object[] snapshot = getArray();
return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
addIfAbsent(e, snapshot);
}javaCopyOnWrite 是一种典型的以空间换时间的并发策略。它通过牺牲写性能与内存开销,换取了极高的读性能和线程安全的简洁实现。在高并发、读操作频繁写操作较少的场景中,CopyOnWriteArrayList 和 CopyOnWriteArraySet 都是不二之选。