Java中的并发容器:CopyOnWriteArrayList详解
字数 933 2025-11-05 08:31:58
Java中的并发容器:CopyOnWriteArrayList详解
1. 容器概述与问题背景
在多线程环境下,对传统集合类(如ArrayList)进行并发读写会引发数据不一致或并发修改异常(ConcurrentModificationException)。例如,一个线程正在遍历列表,另一个线程同时修改列表结构(增删元素),就会导致遍历失败。CopyOnWriteArrayList是JUC包提供的线程安全列表,专门解决这类问题。
2. 核心设计思想:写时复制
- 基本逻辑:当需要修改容器(增、删、改操作)时,不直接修改原数组,而是先复制一份当前数组的副本,在副本上执行修改操作,完成后再将原数组引用指向新数组。
- 读写分离:读操作(如get、遍历)始终基于原数组进行,无需加锁;写操作通过复制新数组保证线程安全。这种设计使得读操作极其高效,适用于读多写少的场景。
3. 内部实现机制
public class CopyOnWriteArrayList<E> {
private transient volatile Object[] array; // volatile保证可见性
// 读操作:直接返回元素,无锁
public E get(int index) {
return (E) array[index];
}
// 写操作:加锁复制新数组
public boolean add(E e) {
synchronized (lock) { // 使用重入锁保证原子性
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1); // 复制新数组
newElements[len] = e;
setArray(newElements); // 替换原数组引用
return true;
}
}
}
4. 关键特性详解
- 最终一致性:读操作可能无法立即看到其他线程的修改,但能保证最终看到写入结果(通过volatile的happens-before规则)。
- 迭代器弱一致性:迭代器创建时会保存当前数组快照,遍历过程中不会反映其他线程的修改,但不会抛出ConcurrentModificationException。
- 内存开销:每次写操作都需要复制整个数组,频繁修改时内存占用较大,可能触发GC。
5. 适用场景与局限性
- 适用场景:
- 读操作远多于写操作(如监听器列表、配置信息缓存)。
- 集合规模较小,写操作频率低。
- 不适用场景:
- 写操作频繁或数据量大的场景(复制成本高)。
- 需要实时读取最新数据的场景(读操作可能读取旧数据)。
6. 与同步容器的对比
- Vector/SynchronizedList:通过synchronized同步所有方法,读写的并发性能均较差。
- CopyOnWriteArrayList:读操作完全无锁,写操作通过复制避免阻塞读操作,在读多写少时性能显著优于同步容器。
7. 实战示例
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
// 线程1:循环读取(不受写操作阻塞)
new Thread(() -> {
for (String s : list) { // 迭代器基于初始快照
System.out.println(s);
}
}).start();
// 线程2:添加元素
new Thread(() -> {
list.add("new element"); // 写操作复制新数组
}).start();
总结:CopyOnWriteArrayList通过写时复制策略,以空间换时间,实现了读操作的高并发性。使用时需权衡其读写特性和内存开销,确保符合业务场景需求。