copyonwrite容器采用写时复制+乐观加锁+volatile引用替换策略:写操作独占复制数组并原子更新引用,读操作无锁访问旧数组;适合读多写少、数据量小、一致性要求不高的场景。

CopyOnWrite 容器(如 CopyOnWriteArrayList 和 CopyOnWriteArraySet)在高并发写操作下,并不是“每次写都立即复制整个数组”,而是采用**写时复制(Copy-On-Write)+ 乐观加锁 + 替换引用**的组合策略,核心在于:**只在真正需要修改底层数组时才复制,且复制动作由写线程独占完成,读完全无锁。**
写操作触发复制的时机
只有调用会改变容器结构的方法(如 add、remove、set)时,才会进入复制流程。读操作(get、iterator、size)永远访问当前 volatile 引用指向的旧数组,不加锁也不复制。
- 写线程先获取可重入锁(
ReentrantLock),确保同一时刻只有一个写在执行复制和替换 - 以当前数组为模板,创建一个新数组(长度 ±1 或按需扩容),把原数据拷贝过去
- 在新数组上完成本次修改(比如末尾追加元素、或删除某索引元素)
- 用
volatile写语义,将容器内部的数组引用原子地更新为新数组
复制过程本身是单线程的,但开销可控
复制数组本质是 Arrays.copyOf(),即底层 System.arraycopy(),是 JVM 高度优化的本地操作,速度很快。但代价是内存——写操作期间,新老数组并存,瞬时内存占用翻倍(尤其大数组)。所以它适合:读多写少、元素数量不大、对实时一致性要求不高的场景。
- 不会为每一次 add 都新建数组 —— 多个连续写操作会被串行化,每个写各自复制一次,互不影响
- 没有“批量合并写”的优化;也没有后台异步复制 —— 复制始终同步、阻塞、由发起写的线程承担
- 迭代器持有的是快照数组,因此遍历时即使其他线程写了,也不会抛
ConcurrentModificationException,也看不到新写入的元素
为什么不怕并发写冲突?
靠的是锁 + volatile 引用更新的双重保障:
-
ReentrantLock排除了多个写线程同时进入复制逻辑的可能 - volatile 数组引用保证:一旦新数组被写入,所有后续读线程都能立即看到最新引用(无需锁),且能安全访问其内容(数组本身不可变)
- 旧数组不再被任何写操作修改,只供读线程继续使用,直到被 GC 回收
实际性能表现的关键点
高并发写 ≠ 高吞吐写。如果写请求非常密集(比如每毫秒几十次 add),锁竞争会成为瓶颈,吞吐量反而不如 ConcurrentLinkedQueue 或分段锁容器。
- 锁持有时间 = 复制数组耗时 + 修改新数组耗时;数组越大,写越慢
- 写线程越多,排队等待锁的时间越长;读线程完全不受影响
- GC 压力来自频繁丢弃旧数组 —— 若写频次高、数组大,容易触发 Young GC 甚至 Promotion
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











