copyonwritearraylist 读快写慢,因读无锁而写需复制整个数组;适合读多写少场景,不适用于高频修改或强一致性要求的场景。

CopyOnWriteArrayList 是读多写少场景下最省心的线程安全列表,但它的“高性能”只对读操作成立——写操作开销很大,且不适用于实时一致性要求高的场景。
为什么读操作快、写操作慢?
它底层不是锁,而是每次写(add、remove、set)都复制整个数组。读操作直接访问当前数组引用,无锁无同步,所以并发读极快;但写一次就要 Arrays.copyOf 整个底层数组,时间复杂度 O(n),内存占用也翻倍。
- 适合:日志订阅器、监听器列表、配置白名单等几乎不改、只遍历的场景
- 不适合:高频增删的队列、需要强一致性的计数器、实时更新的缓存索引
- 注意:
Iterator是弱一致的——迭代期间其他线程的修改对它不可见,也不会抛ConcurrentModificationException
和 ArrayList + synchronized 或 Collections.synchronizedList 有什么区别?
前者用“写时复制”换读性能,后两者是“读写都加锁”。Collections.synchronizedList 的 iterator() 方法返回的迭代器仍是线程不安全的,遍历时必须手动 synchronized 块包住整个遍历逻辑;而 CopyOnWriteArrayList 的迭代器天然安全,不用额外同步。
-
CopyOnWriteArrayList:读无锁、写重复制、迭代器安全、内存敏感 -
Collections.synchronizedList:读写都阻塞、迭代需显式同步、内存轻量 - 别误以为加了
synchronized就能随便遍历——漏掉同步块,照样ConcurrentModificationException
哪些方法会触发数组复制?
只有明确修改结构或内容的方法才复制,查询类方法完全不复制。要注意 set(int, E) 看似只是替换,但它仍会复制数组;而 get(int)、size()、contains(Object) 全部是无锁直接读。
立即学习“Java免费学习笔记(深入)”;
- 触发复制:
add(E)、add(int, E)、remove(Object)、remove(int)、set(int, E)、clear() - 不触发复制:
get(int)、indexOf(Object)、lastIndexOf(Object)、isEmpty() -
addAll(Collection)是一次性复制,比循环调用add高效得多
容易被忽略的内存与 GC 问题
每次写操作生成新数组,旧数组变成垃圾。如果写得频繁,或者底层数组本身很大(比如存了几万条字符串),GC 压力会明显上升,甚至引发老年代频繁回收。这不是理论风险——在监控告警系统里维护动态规则列表时,就真有团队因此遇到 OutOfMemoryError: GC overhead limit exceeded。
- 检查
size()是否长期稳定:如果列表大小波动大,说明写操作实际不少,CopyOnWriteArrayList可能是反模式 - 避免存大对象:例如不要往里面放
byte[]或深嵌套 JSON 字符串,复制成本指数级上升 - JDK 17+ 中它的迭代器仍是弱一致,不会反映写操作的中间状态——这点常被当成“bug”提 issue,其实文档早写明了
真正要用好它,得先确认“写真的很少”,而不是“我以为它很少”。上线前用 JFR 或 jstat 看看 GC 日志里是否频繁出现数组相关对象晋升,比读十遍源码都管用。











