CopyOnWriteArrayList 通过写时复制数组实现遍历安全,避免 ConcurrentModificationException;读无锁、写加锁、迭代基于快照,适用于读多写少场景,但写频繁、大数据量或需强一致性时不宜使用。

CopyOnWriteArrayList 为什么能避免 ConcurrentModificationException
因为每次写操作(add、remove、set)都会新建一个数组副本,原数组只读,迭代器遍历的始终是快照。所以即使一边遍历一边写,也不会触发 ConcurrentModificationException。
但要注意:这不是“线程安全”的万能解药,它牺牲了写性能和内存,换来了读操作的无锁与遍历安全。
- 适用于读多写少场景(比如监听器列表、配置项缓存)
- 写操作会加全局锁(
ReentrantLock),并发写仍会阻塞 - 迭代器不支持
remove()或add(),调用会抛UnsupportedOperationException - 写操作完成后,新数组才对后续读可见——存在短暂的“最终一致性”,不是实时可见
什么时候不该用 CopyOnWriteArrayList
当出现以下任一情况时,应立刻放弃:CopyOnWriteArrayList:
- 写操作频繁(比如每秒数百次
add)——每次写都复制整个数组,GC 压力陡增 - 集合元素大且多(如
new CopyOnWriteArrayList<byte>()</byte>)——一次add可能触发数 MB 内存拷贝 - 需要强一致性读写(如金融类计数、状态同步)——它不保证写后立即被读到
- 要使用
ListIterator进行双向遍历并修改——它的listIterator()返回的迭代器同样不可变
对比 ArrayList + Collections.synchronizedList 的关键差异
Collections.synchronizedList(new ArrayList()) 对所有方法加同一把锁,读写互斥;而 CopyOnWriteArrayList 读不加锁、写加锁、迭代不阻塞写。
- 高并发读 + 低频写 →
CopyOnWriteArrayList吞吐更高 - 混合读写且写不稀疏 →
synchronizedList实际延迟更低(无数组复制开销) -
size()在CopyOnWriteArrayList中是 O(1),但注意:它返回的是当前快照大小,不反映正在写入但尚未完成的操作 -
contains(Object)是 O(n) 且基于快照,可能查不到刚写入、还未刷新到主数组的元素
一个典型误用:在 for-each 循环里 remove 元素
哪怕用了 CopyOnWriteArrayList,下面这段代码依然不会删掉任何元素:
for (String s : list) {
if (s.startsWith("tmp")) {
list.remove(s); // 这里删的是“下一个快照”,当前遍历的快照不变
}
}
原因在于:remove 触发数组复制,但 for-each 使用的迭代器早已持有旧数组引用,循环继续遍历原快照。真正生效的删除,只影响后续迭代或读操作。
如果真需要边遍历边清理,必须改用显式索引 + get(i) 判断,或改用其他结构(如 ConcurrentLinkedQueue 配合过滤逻辑)。
最常被忽略的一点:它解决不了“业务逻辑层面的竞态”,比如两个线程同时判断 list.size() == 0 再 add,仍可能导致重复添加——这得靠更高层的协调机制,不是容器本身能兜底的。

















