Java并发编程中CopyOnWrite是什么_适用场景分析

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

CopyOnWriteArrayList 为什么适合读多写少的场景

因为每次写操作(addremoveset)都会复制整个底层数组,写入新数组后再原子替换引用,所以写性能差、内存开销大;但读操作完全无锁,直接访问当前数组引用,零同步开销。这决定了它只在「读远多于写」且不能接受读阻塞时才值得用。

  • 典型适用:监听器列表(如 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 压力剧增。

// 危险!不要这么写
CopyOnWriteArrayList list = 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,依然存在竞态:

if (!list.contains("key")) {
    list.add("key"); // 这两步之间,别的线程可能已加过
}
  • 这不是 CopyOnWriteArrayList 的缺陷,而是所有无锁容器的共性
  • 若需真正原子的「不存在才加」,得用 ConcurrentHashMap.computeIfAbsent 或加显式锁
  • 别被「线程安全」四个字带偏——它只保方法级原子,不保业务逻辑级原子
实际用的时候,最常被忽略的是「写放大」和「快照语义」这两点。前者导致线上 OOM,后者导致排查问题时发现“刚加的数据怎么迭代不出来”,但日志里明明写了 add 成功。