copyonwritearraylist写操作慢因每次增删改均需复制整个数组(o(n)),读操作直接访问volatile引用的数组故无锁;适合读远多于写的场景,写频繁时gc压力大。

CopyOnWriteArrayList 为什么写操作慢但读不加锁
因为每次 add、set、remove 都会新建数组,把原数组内容复制过去再修改——写是“拷贝+替换”,读只是拿当前引用的数组地址,天然线程安全且无同步开销。
适合读远多于写的场景,比如监听器列表、配置项快照。如果写频繁(如每秒几百次),GC 压力和内存抖动会明显上升。
-
CopyOnWriteArrayList的get(int)方法不加锁,也不做任何 volatile 读,直接数组下标访问 - 写操作最终调用
Arrays.copyOf,意味着一次完整的堆内存分配和复制,时间复杂度 O(n) - 迭代器基于创建时的数组快照,所以遍历时即使其他线程修改了容器,也不会抛
ConcurrentModificationException,但也看不到新元素
CopyOnWriteArraySet 底层其实依赖 CopyOnWriteArrayList
它没有自己实现增删逻辑,而是用 CopyOnWriteArrayList 存储元素,并在 add 时先查重再插入——这意味着两次遍历:一次 contains,一次 add。
查重靠的是 equals,不是哈希,所以性能比 ConcurrentHashMap 差很多,尤其元素多时。
立即学习“Java免费学习笔记(深入)”;
- 构造
CopyOnWriteArraySet时传入已有集合,会逐个调用add,而不是批量初始化,容易被误以为“能高效构建” - 没有
removeIf或批量删除方法,所有删除都走单次remove(Object),内部仍要遍历数组 - 不能替代
ConcurrentHashMap.keySet()做去重容器,除非你明确需要迭代安全 + 写少读多
“读写分离”不是指读写线程分组,而是指读写操作对数据副本的使用策略
很多人以为 CopyOnWrite 是让读线程和写线程各玩各的,其实不是:所有线程看到的都是同一个数组引用,只是写操作会原子性地切换这个引用指向新数组。
关键点在于 array 字段是 volatile 的,保证新数组对所有读线程可见;但读线程永远不关心“谁在写”,只管读当前引用。
- 不存在“读线程池”或“写线程池”的设计,线程角色完全由调用的方法决定
- 写操作的原子性仅体现在引用更新上,中间复制过程不对外暴露,所以不会出现“读到一半新数组、一半旧数组”的情况
- 如果在写操作中途发生 OOM,旧数组仍完好,容器状态不变——这是它比某些 CAS 重试方案更稳的地方
别在循环里反复调用 size() 或 toArray() 当作“实时长度”用
size() 虽然快,但它返回的是当前数组长度;而 toArray() 每次都新建数组并复制,开销等同于一次写操作的前半段。
常见错误是在监控日志里每秒调 list.toArray().length,结果把本该轻量的读操作变成了隐式写压力。
- 想判断是否为空,直接用
isEmpty(),它只是array.length == 0,比size() == 0少一次字段读取 - 需要转成数组做计算?先存一次引用:
Object[] snapshot = list.toArray(),再复用,避免重复复制 - 注意
toArray(T[])的重载行为:传入非空数组时可能复用其空间,但 CopyOnWriteArrayList 总是新建数组,所以这个重载没意义










