CopyOnWriteArrayList 为什么适合读多写少?读写锁怎么选?
结论先行:CopyOnWriteArrayList 在写时复制整个数组,读操作完全无锁——所以读的性能接近普通 ArrayList,写则要付出 O(n) 的复制代价。它只适合"读极多、写极少、元素不多"的场景;写频繁或数据量大时,应该换成读写锁方案。
1. 写时复制的机制
写操作(add/set/remove):
array ──▶ [A][B][C] ← 读线程此刻仍在读这个旧数组,不受影响
│ 复制一份
▼
[A][B][C][D] ← 在新数组上修改
│
▼
array = 新数组 ← volatile 写,一次性切换引用
读操作(get/iterator):
直接读 array 引用指向的数组,不加锁、不阻塞
两个关键点:
- 写加锁、读不加锁:写操作用
ReentrantLock串行化,保证不会有两个线程同时复制; - 数组引用是 volatile:保证新数组对读线程立即可见,读线程要么看到旧数组、要么看到新数组,不会看到"改了一半"的状态。
2. 与其它方案的对比
| 方案 | 读 | 写 | 适用场景 |
|---|---|---|---|
CopyOnWriteArrayList | 无锁,O(1) | 加锁 + 复制数组,O(n) | 读极多写极少、元素少 |
ReentrantReadWriteLock + ArrayList | 读锁共享,有锁开销 | 写锁独占 | 读写都比较频繁、读多写少 |
Collections.synchronizedList | 全程互斥 | 全程互斥 | 并发很低 |
ConcurrentHashMap | 桶级并发 | 桶级锁 | 本质是 key-value,不是列表 |
3. 用法与迭代器
CopyOnWriteArrayList<Listener> listeners = new CopyOnWriteArrayList<>();
// 读:完全无锁,可以在遍历中安全地增删
for (Listener l : listeners) {
l.onEvent(event); // 遍历的是快照,不会抛 ConcurrentModificationException
}
// 写:每次 add 都复制一份新数组
listeners.add(new SmsListener());
// 批量写务必用 addAll,避免 N 次复制
listeners.addAll(Arrays.asList(a, b, c));
迭代器的语义值得单独记:它拿到的是创建迭代器那一刻的数组快照,之后别人怎么改都不影响这次遍历。所以它弱一致——遍历期间新增的元素看不到,但也不会报错。
4. 什么时候不该用
| 场景 | 为什么不适合 | 替代 |
|---|---|---|
| 写操作频繁 | 每次写都复制 O(n),CPU 和 GC 双杀 | ReentrantReadWriteLock |
| 列表元素很多(上万) | 一次写要复制几万个引用 | 分段结构或读写锁 |
需要 size() 精确即时 | size() 读的是快照值 | 加锁统计 |
| 需要按下标做批量修改 | 每次修改都触发复制 | 换成普通列表 + 锁 |
一个典型的滥用:把 CopyOnWriteArrayList 当作"线程安全的 ArrayList"随手用在写多读少的队列场景,结果 CPU 全耗在数组复制上。
5. 常见追问
追问一:为什么它的迭代器不会抛 ConcurrentModificationException?
因为它迭代的是旧数组,修改者改的是新数组,两者互不影响。代价是遍历期间看不到最新数据(弱一致性)。
追问二:写的时候为什么要复制整个数组,不能只改一个下标? 因为读线程可能正在遍历旧数组。如果原地修改,读线程会看到"一半新一半旧"的数组。复制换来的是读路径完全无锁,这是它的核心取舍。
追问三:CopyOnWriteArraySet 也是这个原理吗?
是,内部就是一个 CopyOnWriteArrayList,add 时先线性查找是否存在。所以它的 add 和 contains 都是 O(n)——元素多时性能很差。
追问四:它和读写锁怎么选? 看两个数字:读写的比例和每次操作的数据量。读操作占比 95% 以上、元素在几百个以内 → COW;读写都频繁或元素上千 → 读写锁。再多考虑一层:COW 的读是零锁开销,读写锁的读仍要 CAS 修改状态,高并发下也会有竞争。
追问五:COW 适合放什么? 监听器列表、白名单/黑名单、路由表、配置快照——共同特征是变更靠运维动作触发,读发生在每个请求上。
小结
CopyOnWriteArrayList 用"写时复制的成本"换"读时零锁的自由"。判断能不能用只需一问:这个列表是每读一百次才写一次吗?是就用,不是就别用。