写时复制的艺术:CopyOnWriteArrayList深度解析
文章目录
从生活中的复印机到Java并发容器
想象一下办公室里的共享文件场景:当多位同事需要同时查阅和修改同一份文档时,传统的做法是大家轮流等待使用原件,这显然效率低下。而现代办公室通常会采用另一种方案——每当有人需要修改时,就复印一份副本,在副本上修改,最后再将修改好的版本设为新的"原件"。这种"写时复制"的思路正是CopyOnWriteArrayList的设计哲学。
在Java并发编程的世界里,List接口的线程安全实现一直是个挑战。传统的Vector虽然线程安全,但它的同步机制过于粗暴——就像给整个文档库上了锁,每次只允许一个人操作。而Collections.synchronizedList()包装器也好不到哪去,虽然灵活些,但本质上还是"全锁"策略。CopyOnWriteArrayList则另辟蹊径,它采用了我们开头提到的复印机策略:读操作完全无锁,可以并发进行;写操作则通过复制整个底层数组来实现线程安全。
这种设计带来了惊人的读取性能,特别适合"读多写少"的场景。就像公司里查阅规章制度的人远多于修订制度的人一样,很多实际业务场景也遵循这个规律。但值得注意的是,它并非万能药,不当使用反而会导致性能问题。接下来,我们将深入剖析这个与众不同的并发容器,了解它的内部机制、适用场景以及使用时的注意事项。
写时复制机制原理解析
CopyOnWriteArrayList的核心秘密藏在它的名字里——“写时复制”(Copy-On-Write)。这个机制就像我们平时编辑重要文档时的版本控制策略:你永远不会直接修改原始文件,而是先创建一个副本,所有的修改都在副本上进行,确认无误后再替换原始文件。这种保守策略虽然会消耗一些额外的空间和时间来创建副本,但却换来了读取操作的无锁并发能力。
深入源码,我们可以看到这个类内部维护了一个volatile修饰的数组引用:
// CopyOnWriteArrayList的核心存储结构
private transient volatile Object[] array;
volatile关键字确保了数组引用的内存可见性,这是实现无锁读取的关键。当需要修改列表时(如add、set、remove等操作),实现步骤非常规范:
- 获取当前数组引用
- 创建新数组(长度根据需要调整)
- 将原数组内容拷贝到新数组
- 在新数组上执行修改
- 将内部array引用指向新数组
这个过程通过显式锁(ReentrantLock)保证原子性,下面是add方法的简化版实现:
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(); // 释放锁
}
}
读取操作则完全不需要同步,因为array引用是volatile的,且每次读取都会拿到一个确定的数组快照:
public E get(int index) {
return get(