CopyOnWriteArrayList适合读多写少场景,因写操作需复制整个数组导致性能差、内存开销大,而读操作无锁、零同步开销;仅适用于读远多于写且不可接受读阻塞的场景。

CopyOnWriteArrayList 为什么适合读多写少的场景
因为每次写操作(add、remove、set)都会复制整个底层数组,写入新数组后再原子替换引用,所以写性能差、内存开销大;但读操作完全无锁,直接访问当前数组引用,零同步开销。这决定了它只在「读远多于写」且不能接受读阻塞时才值得用。
- 典型适用:监听器列表(如
Swing事件通知)、配置白名单缓存、实时统计中的只读快照 - 不适用:高频增删的队列、需要强一致性写顺序的场景(
CopyOnWriteArrayList的迭代器是快照,看不到其他线程刚做的写) - 注意:
size()和get(int)是 O(1),但contains(Object)是 O(n) 且遍历的是快照,可能查不到最新写入的元素
CopyOnWriteArraySet 底层其实依赖 CopyOnWriteArrayList
CopyOnWriteArraySet 没有独立的数据结构,而是用 CopyOnWriteArrayList 实现的 —— 所有添加操作都先调用 list.contains(element) 判断是否存在,再决定是否 list.add(element)。这意味着:
- 每次
add至少一次 O(n) 遍历(查重),比HashSet的 O(1) 差很多 - 它不保证元素自然顺序或插入顺序(底层是数组,但查重逻辑不维护顺序语义)
- 如果业务本就需要去重 + 读多写少 + 迭代安全,可以用;否则优先考虑
Collections.synchronizedSet(new HashSet())或ConcurrentHashMap.newKeySet()
常见误用:把它当普通 ArrayList 用,结果内存爆了
很多人没意识到 CopyOnWriteArrayList 的写操作会触发全量数组复制。比如在循环中反复 add 10 万条数据,就会产生 10 万次数组拷贝,内存占用飙升,GC 压力剧增。
// 危险!不要这么写 CopyOnWriteArrayListlist = new CopyOnWriteArrayList<>(); for (int i = 0; i < 100000; i++) { list.add("item-" + i); // 每次都复制前一个数组 → 内存爆炸 }
- 正确做法:先用普通
ArrayList构建好数据,再一次性转成CopyOnWriteArrayList - 或者改用
ConcurrentLinkedQueue+ 定期快照,视具体读一致性要求而定 - JVM 参数如
-XX:+UseG1GC对缓解其 GC 压力帮助有限,根本解法是控制写频次和数据规模
它无法解决复合操作的原子性问题
CopyOnWriteArrayList 的单个方法(如 add)是线程安全的,但多个操作组合就不行。例如「检查是否存在再添加」这种典型的 check-then-act,依然存在竞态:
立即学习“Java免费学习笔记(深入)”;
if (!list.contains("key")) {
list.add("key"); // 这两步之间,别的线程可能已加过
}
- 这不是
CopyOnWriteArrayList的缺陷,而是所有无锁容器的共性 - 若需真正原子的「不存在才加」,得用
ConcurrentHashMap.computeIfAbsent或加显式锁 - 别被「线程安全」四个字带偏——它只保方法级原子,不保业务逻辑级原子











