Copy-On-Write
Copy-On-Write(写时复制)是计算机领域相当经典的优化思想。如果问一个 Java 开发者 copy-on-write 有什么作用?他们往往第一反应就是:优雅地解决读多写少场景下的并发问题。
背景
众所周知,多线程环境下会出现 data race 的问题。以 Java 中的 ArrayList 为例,ArrayList 本身是不保证线程安全的。通常情况,要保证多线程环境下不出问题,就要给 ArrayList 加上读写锁:
- 读要读锁
- 写要写锁
- 读与读之间不互斥
- 读与写之间要互斥
- 写与写之间也要互斥
然而,对于读多写少的场景来说,频繁地读取必然导致频繁地加锁,而与写互斥的情况却很少出现,这似乎有点不"经济"。
CopyOnWriteArrayList
这时候 CopyOnWriteArrayList(以下简称 COWList)就登场了。
COWList 的总体设计思想是:在读的过程中去掉了锁,而在写的过程中则需要引入互斥锁,但是这个锁不会影响到读本身,也就进一步释放了读的性能瓶颈。
核心源码分析
// 只截取了部分 CopyOnWriteArrayList 代码片段
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
/**
* 读操作 —— 无锁
*/
public E get(int index) {
return get(getArray(), index);
}
/**
* 写操作 —— 互斥锁
*/
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();
}
}
三个关键点
- COWList 读操作是无锁的
- COWList 写与写之间是互斥的
- 底层持有的数组变量 array 是通过 volatile 修饰的
JMM 的 happens-before 语义保证了 volatile 变量的内存可见性,这使得任意线程在任意时间点读取 array 变量的时候都能保证读到最新值。
而写的过程中,除了互斥保证以外,还需要将 array 数组拷贝一个副本出来,对副本进行修改后再将该副本的引用赋值给 array 变量,以替换原先的引用,而这个替换过程是原子性的。