copyonwritearraylist适合读远多于写、写不频繁且不要求实时可见的场景,如监听器列表、配置快照、日志handler集合;写频繁或需强一致性时不适用,add/set均触发全数组复制,迭代器只读且不支持remove。

CopyOnWriteArrayList 适合哪些场景 它只适合「读远多于写」且「写操作不频繁、不要求实时可见」的场景。比如监听器列表、配置项快照、日志收集器的 handler 集合——这些地方读取可能每毫秒发生多次,但添加或删除监听器一小时才一次。
如果写操作较频繁(比如每秒几次以上),CopyOnWriteArrayList 会持续触发数组复制,CPU 和 GC 压力明显上升;如果业务依赖写入后立刻能被读到(比如状态同步),它也不适用,因为读线程看到的是旧快照。
常见错误现象:ConcurrentModificationException 消失了,但新添加的元素在下一次写之前始终读不到;或者压测时 add() 耗时突增,GC 日志里出现大量短生命周期大数组。
add() 和 set() 的行为差异很关键
add() 总是复制整个底层数组,再追加;set(int index, E element) 同样会复制——哪怕只是改一个元素。这点和普通 ArrayList 完全不同,后者 set() 是 O(1) 且无拷贝。
-
add()和remove()都走同一套复制逻辑:锁 + 复制 + 替换引用 -
set()看似轻量,实际开销和add()接近,别误以为“改一个值就快” - 遍历中调用
add()不会抛异常,但新增元素对当前遍历不可见——这是设计使然,不是 bug
迭代器不支持 remove() 是故意的
CopyOnWriteArrayList.iterator() 返回的 Iterator 是只读的,调用 iterator.remove() 会直接抛 UnsupportedOperationException。这不是遗漏,而是为了保证“写时复制”模型不被破坏——如果允许边遍历边删,就得在迭代器里维护额外状态,违背了无锁读的设计初衷。
如果你真需要边遍历边删,得换思路:
- 先收集要删的元素(如
toRemove列表),遍历完再调removeAll(toRemove) - 改用
ConcurrentHashMap存索引+值,用 key 控制存取逻辑 - 确认是否真的需要并发安全——有时加个
synchronized块更简单可靠
和 Collections.synchronizedList 的性能对比
在纯读场景,CopyOnWriteArrayList 比 Collections.synchronizedList(new ArrayList()) 快得多,因为前者读完全无锁;后者每次 get() 都要进同步块。
但在混合读写场景,尤其写稍多时,synchronizedList 反而更稳——没有数组复制开销,也没有内存抖动。JMH 测试显示,当写占比超 5%,CopyOnWriteArrayList 的吞吐常低于前者。
容易忽略的一点:CopyOnWriteArrayList 的内存占用永远是「当前数据 + 上次写操作产生的旧数组」,GC 压力藏在背后。线上如果发现老年代增长异常平缓但 CMS GC 频繁,值得查查有没有滥用它。










