CopyOnWriteArrayList适合读多写少场景,读操作无锁、线程安全、迭代器不抛ConcurrentModificationException;写操作复制整个数组、开销大、内存翻倍、ReentrantLock串行化。

CopyOnWriteArrayList 适合读多写少的并发场景
它只在写操作(add、remove、set)时复制整个底层数组,读操作(get、iterator、size)完全无锁。这意味着:读性能极高、线程安全、迭代器不会抛 ConcurrentModificationException;但写操作开销大、内存占用翻倍、且写操作之间互斥(本质是用 ReentrantLock 串行化)。
典型适用场景包括:
- 监听器列表(如 GUI 事件回调、Spring ApplicationListener),注册/注销极少,遍历通知极频繁
- 配置项白名单、状态枚举缓存等静态或低频变更的只读集合
- 日志收集器中的 handler 列表,启动后基本不增删
不适合:高频增删、实时性要求强(写操作延迟不可控)、内存敏感(每次写都新建数组,旧数组待 GC)。
为什么不能用它替代 ArrayList 或 ConcurrentHashMap
CopyOnWriteArrayList 和 ArrayList 不是简单“线程安全版”,而是设计哲学完全不同:
立即学习“Java免费学习笔记(深入)”;
-
ArrayList是可变、非线程安全、零拷贝;CopyOnWriteArrayList是写时复制、读快写慢、天然弱一致性(读不到最新写入) -
ConcurrentHashMap支持高并发读写,但它是 Map;若你需要的是「有序、可重复、支持随机索引访问」的并发 List,它无法替代 - 它不支持
list.iterator().remove()—— 迭代器的remove()方法直接抛UnsupportedOperationException
常见误用:用它存实时订单列表、高频更新的用户会话 ID 集合——结果是 GC 压力陡增、写吞吐骤降、数据延迟明显。
底层 add() 操作到底做了什么
调用 add(E) 时,会触发完整数组复制流程:
final Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements);
关键点:
- 复制开销与当前数组长度成正比,不是常数时间;10 万元素时一次
add可能分配并拷贝 ~800KB 内存 -
setArray()使用volatile写,保证新数组对所有线程可见,但不保证之前所有写操作的全局顺序(JMM 层面) - 所有写方法(
add、remove、set)共用同一把ReentrantLock,写操作完全串行,不存在并发写竞争,但也失去了写并行能力
迭代器返回的是快照,不是实时视图
iterator() 返回的 COWIterator 在构造时就持有了当前数组的引用副本,后续无论原列表如何修改,迭代器看到的始终是创建那一刻的状态。
这导致两个易忽略行为:
- 在迭代过程中调用
list.add(x),该元素一定不会出现在本次迭代中 - 多个线程同时迭代同一个
CopyOnWriteArrayList,彼此看到的“快照”可能来自不同写操作之后,彼此之间没有 happens-before 关系
所以它适合“遍历时不关心中间变更”的场景,比如广播通知、批量导出快照——不适合实现类似“边遍历边过滤删除”的逻辑(应改用 ConcurrentLinkedQueue 或加显式锁)。










