了解并发容器 CopyOnWriteArrayList
了解并发容器 CopyOnWriteArrayList
一、CopyOnWriteArrayList 概述
-
什么是 CopyOnWriteArrayList?
- Java 并发包 (
java.util.concurrent) 中的线程安全容器 - 基于「写时复制」(Copy-On-Write)机制实现
- 与
ArrayList的核心区别:读写分离的线程安全策略
- Java 并发包 (
-
设计目标与适用场景
- 解决多线程环境下遍历与修改的冲突
- 读多写少场景的高效实现(如:事件监听器列表、配置管理)
-
与普通 ArrayList 的对比
- 线程安全性差异(
ArrayList非线程安全) - 迭代器行为区别(
CopyOnWriteArrayList迭代器不抛出ConcurrentModificationException)
- 线程安全性差异(
| 对比维度 | CopyOnWriteArrayList | ArrayList |
|---|---|---|
| 线程安全性 | ✅ 线程安全(基于写时复制和锁机制) | ❌ 非线程安全(需外部同步如 Collections.synchronizedList) |
| 底层实现 | 写操作时复制新数组(volatile 数组 + ReentrantLock) |
动态数组(Object[] 无锁,依赖 modCount 机制) |
| 修改操作 | 写操作加锁,每次修改都会复制整个数组(O(n) 时间) | 直接修改原数组(O(1) 追加,O(n) 插入/删除) |
| 迭代器行为 | 弱一致性:迭代器遍历创建时的数组快照,不抛异常 | 快速失败(ConcurrentModificationException) |
| 读操作性能 | ⚡ 极高(无锁,直接访问数组) | ⚡ 高(但多线程需同步时性能下降) |
| 写操作性能 | ⚠️ 低(需复制数组,大数据量时内存和性能开销大) | ⚡ 高(无复制,但多线程需同步时性能下降) |
| 内存开销 | 高(每次写操作复制数组,旧数组可能未被及时 GC) | 低(直接操作原数组) |
| 适用场景 | 读多写少(如事件监听器、黑白名单) | 单线程环境,或需手动控制同步的多线程环境 |
| 异常风险 | 迭代器不会抛出 ConcurrentModificationException |
并发修改时迭代器可能抛出异常 |
| 批量操作优化 | 批量写入建议用 addAll 减少复制次数 |
无特殊优化,直接操作原数组 |
二、内部实现原理
CopyOnWriteArrayList 的线程安全是通过“写时复制机制”实现的。CopyOnWriteArrayList 的写操作通过 锁(ReentrantLock) 和 volatile 数组引用 保证线程安全。
-
写时复制(Copy-On-Write)机制
- 写入操作(add/set/remove):复制底层数组 → 修改副本 → 替换原数组
- 读取操作:直接访问原数组(无锁)
-
底层数据结构
- 使用
volatile数组引用(private transient volatile Object[] array;),保证多线程下数据的可见性。 - 通过
ReentrantLock保证写操作的原子性。同一时间只有一个线程能持有锁,其他线程必须等待锁释放。
- 使用
-
线程安全性实现
- 写操作加锁(避免多个线程同时修改)
- 读操作无锁(依赖数组引用的
volatile特性)
三、核心 API 与使用示例
-
构造函数
CopyOnWriteArrayList():创建空列表CopyOnWriteArrayList(Collection<? extends E> c):从集合初始化
-
常用方法
- 写操作:
add(),addIfAbsent(),remove(),set() - 读操作:
get(),size(),iterator() - 批量操作:
addAllAbsent(),removeAll()
- 写操作:
-
api使用示例
// 创建实例 CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(); // 多线程添加元素 Runnable writer = () -> list.add(Thread.currentThread().getName()); new Thread(writer).start(); new Thread(writer).start(); // 安全遍历(迭代器访问旧数组) Iterator<String> it = list.iterator(); while (it.hasNext()) { System.out.println(it.next()); }
四、代码示例说明写时复制机制
场景:两个线程同时向 CopyOnWriteArrayList 添加元素。
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
// 线程1:添加元素 "A"
Thread t1 = new Thread(() -> {
list.add("A");
System.out.println("线程1添加完成,当前数组:" + list);
});
// 线程2:添加元素 "B"
Thread t2 = new Thread(() -> {
list.add("B");
System.out.println("线程2添加完成,当前数组:" + list);
});
t1.start();
t2.start();
// 输出可能:
// 线程1添加完成,当前数组:[A]
// 线程2添加完成,当前数组:[A, B]
2. 流程图与代码执行步骤对照
sequenceDiagram
participant 线程1
participant 锁
participant 数组
participant 线程2
线程1->>锁: 请求加锁(成功)
线程1->>数组: 复制原数组 [] → 新数组 ['A']
线程1->>数组: 替换引用为 ['A']
线程1->>锁: 释放锁
线程2->>锁: 请求加锁(阻塞等待...)
线程2->>锁: 加锁成功(获取到线程1修改后的数组 ['A'])
线程2->>数组: 复制新数组 ['A'] → ['A', 'B']
线程2->>数组: 替换引用为 ['A', 'B']
线程2->>锁: 释放锁
步骤1. 线程1先获取锁
// 线程1执行 add("A")
final ReentrantLock lock = this.lock;
lock.lock(); // 线程1获取锁成功
try {
Object[] elements = getArray(); // 原数组为 []
Object[] newElements = Arrays.copyOf(elements, elements.length + 1); // 复制为 ["A"]
newElements[elements.length] = "A";
setArray(newElements); // 替换数组引用为新数组 ["A"]
} finally {
lock.unlock(); // 释放锁
}
步骤2. 线程2等待锁释放
- 线程2调用
add("B")时,发现锁已被线程1持有,进入阻塞状态。 - 直到线程1释放锁后,线程2才能继续执行。
步骤3. 线程2获取锁并操作
// 线程2执行 add("B")
lock.lock(); // 线程1释放锁后,线程2获取锁成功
try {
Object[] elements = getArray(); // 此时原数组是线程1修改后的 ["A"]
Object[] newElements = Arrays.copyOf(elements, elements.length + 1); // 复制为 ["A", "B"]
newElements[elements.length] = "B";
setArray(newElements); // 替换数组引用为新数组 ["A", "B"]
} finally {
lock.unlock();
}
锁机制 保证了写操作的原子性和串行性,volatile 数组引用 确保线程间的修改可见性。因此,线程2的写操作总是在线程1的修改结果基础上进行,不会出现数据覆盖或丢失。
五、总结
CopyOnWriteArrayList 写时复制机制特性如下:
- 写操作原子性:通过锁保证同一时间只有一个线程修改数组。
- 数据隔离性:读操作始终访问旧数组,直到写操作完成替换。
- 内存可见性:
volatile修饰的数组引用确保修改对其他线程立即可见。
写时复制机制确保了 CopyOnWriteArrayList 的 读操作无锁且线程安全,但 写操作成本高昂,需根据场景权衡使用。

浙公网安备 33010602011771号