CopyOnWriteArrayList写时复制与读写分离的高并发读源码解析
CopyOnWriteArrayList通过写时复制和读写分离实现线程安全,读操作无锁且不阻塞,写操作加锁并复制数组后替换引用,牺牲写性能换取高并发读性能,适用于缓存、配置中心等读多写少场景。
在之前的文章里,我们深入剖析过 ArrayList 的动态数组与扩容机制,也明确了它与 LinkedList 之间的选型边界。不过,有一个核心问题始终悬而未决:ArrayList 并非线程安全,一旦进入高并发场景,该由谁来顶上?
有人会想到用 Collections.synchronizedList 去包装 ArrayList,但它的全量同步锁导致读写操作互相阻塞,高并发读场景下性能直接跳水。也有人搬出 Vector,可它的方法级同步锁本质上仍是重锁,早已跟不上现代高并发的节奏。而 CopyOnWriteArrayList,作为 ArrayList 的线程安全升级版,依靠“写时复制(COW)”这一核心设计,在高并发读、低并发写场景下,漂亮地平衡了线程安全与性能。它几乎是缓存、配置中心、消息订阅这类“读多写少”场景的最优解,也是后端面试中并发集合模块的常客。

这篇文章将延续“痛点开篇 + 通俗类比 + 源码精读 + 实战避坑”的风格,从核心设计思想到底层源码实现,从读写流程拆解到场景选型,彻底搞懂 CopyOnWriteArrayList。
写时复制到底是怎么回事?它是如何实现读写分离、做到读操作无锁还能保证线程安全的?写操作背后的加锁逻辑与数组拷贝,又藏着怎样的性能代价?它和 ArrayList、SynchronizedList、Vector 到底有哪些本质差异?哪些场景是它的主场,哪些场景又绝对不能碰?
我们直接深入主题。
一、开篇痛点:高并发下,ArrayList 的替代方案都有哪些坑?
在正式分析 CopyOnWriteArrayList 之前,先直面高并发操作 List 时的核心痛点:ArrayList 非线程安全,直接使用容易导致数据错乱、数组越界甚至空指针问题。而传统的线程安全替代方案,各有各的性能硬伤。
先看一组高并发测试数据(基于 JDK8,100 个读线程 + 10 个写线程,操作 10 万条数据),各方案的表现差异一目了然:
传统线程安全 List 的核心问题在于读写互斥:哪怕读操作本身不会修改任何数据,也会被写操作的同步锁死死挡住。而在高并发场景下,读操作占比往往超过 90%,这种设计必然严重浪费 CPU 资源,形成性能瓶颈。
CopyOnWriteArrayList 的“写时复制”设计,则彻底打破了读写互斥的枷锁。读操作完全无锁、无阻塞,写操作单独加锁,双方基于不同的数组副本执行,从根源上解决了高并发读的性能难题。这正是它成为缓存、配置中心、静态列表等“读多写少”场景首选的根本原因——用写操作的那点性能代价,换取读操作的极致体验,典型的“空间换时间”思路。
二、核心思想:写时复制(COW)+ 读写分离
要真正理解 CopyOnWriteArrayList,先要吃透它的核心设计思想——写时复制(Copy On Write,简称 COW)。这是一种在高并发、分布式系统中广泛应用的模式,核心就四个字:读共享,写复制。
1. 通俗类比:图书馆的“复印资料”模式
把 CopyOnWriteArrayList 想象成一个图书馆。里面的藏书是底层数据数组,读者查阅是读操作,管理员新增、修改或删除书籍是写操作。
读操作就像读者直接从书架上拿书看,不用排队,不用登记,多人可以同时看同一本书——无锁、无阻塞,效率极高。
写操作则不同:管理员不会直接在书架上添加新书,因为那会影响到正在查阅的读者。他会先把书架上的所有书复印一份,形成一个新的书架副本,在副本上添加新书。完成后,再将图书馆的“主书架”替换成这个新副本。整个过程需要上锁,以保证同一时间只有一个管理员操作。
这样一来,读写就分离了:读操作基于旧书架副本,写操作基于新书架副本,互不打扰。哪怕写操作正在进行,读者也能正常查阅旧书,不会被阻塞。
这个类比精准对应了 CopyOnWriteArrayList 的核心特性:
读无锁:读操作直接访问底层数组,没有任何同步机制;
写加锁:保证同一时间只有一个写操作,避免多个写操作创建多个副本导致数据混乱;
写时复制:写操作从不修改原数组,而是创建副本修改,再替换原数组;
最终一致性:读操作可能会短暂读到“旧数据”,但最终会看到最新数据——这是最终一致性,而非强一致性。
2. 核心原理:三句话讲清底层逻辑
基于写时复制思想,CopyOnWriteArrayList 的底层实现可以浓缩为三个要点,也是它与 ArrayList 的本质区别:
底层存储:与 ArrayList 一样基于 Object 数组,但数组被 volatile 修饰——保证数组引用的可见性,写操作替换数组后,所有读线程能立即看到最新引用;
读操作:直接访问当前数组副本,无锁、无校验、无阻塞,时间复杂度 O(1),和 ArrayList 的读操作完全一致;
写操作:执行“加锁 → 复制原数组 → 在副本上修改 → 替换原数组 → 解锁”的完整流程,全程加锁保证线程安全,复制数组则带来额外的时间和内存开销,时间复杂度 O(n)。
关键的亮点在于:volatile 修饰的是数组引用,而非数组元素。数组引用的修改会立刻对所有线程可见,但数组元素的修改不会。而 CopyOnWriteArrayList 的写操作从不修改原数组,只替换数组引用,恰好完美利用了 volatile 的可见性特性。
三、核心结构:与 ArrayList 的核心差异
CopyOnWriteArrayList 的核心结构比 ArrayList 更简洁,仅靠几个关键属性就实现了写时复制与线程安全。下面基于 JDK8 源码拆解,并与 ArrayList 进行对比。
1. 核心属性源码(JDK8 精简版)
public class CopyOnWriteArrayList
implements List, RandomAccess, Cloneable, ja va.io.Serializable {
// 写操作的核心锁:保证同一时间只有一个写操作,ReentrantLock 比 synchronized 更灵活
final transient ReentrantLock lock = new ReentrantLock();
// 核心:存储元素的底层数组,volatile 修饰保证数组引用的可见性
// 注意:数组本身是不可变的,写操作只会替换这个引用
private transient volatile Object[] array;
// 获取当前底层数组
final Object[] getArray() {
return array;
}
// 设置底层数组
final void setArray(Object[] a) {
array = a;
}
// 无参构造:初始化空数组
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
// 集合构造:将传入集合的元素复制到新数组
public CopyOnWriteArrayList(Collection extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList>)c).getArray();
else {
elements = c.toArray();
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
} 2. 与 ArrayList 核心属性对比(关键差异)
与 ArrayList 相比,CopyOnWriteArrayList 没有扩容机制、没有 modCount 字段,也没有默认容量。它的数组长度始终等于元素个数,写操作时直接按需创建新数组。锁机制方面,ArrayList 无法用于并发,而 CopyOnWriteArrayList 通过 ReentrantLock 保护写操作,读操作则完全无锁。
3. 核心结论
数组不可变:CopyOnWriteArrayList 的底层数组是“只读”的,所有写操作都不会修改原数组,只会创建新数组并替换引用——这是读操作无锁的核心前提。
锁只为写服务:ReentrantLock 仅在写操作时使用,读操作完全无锁,彻底实现读写分离。volatile 保证引用可见性:数组引用被 volatile 修饰,确保写操作替换数组后,所有读线程能立刻看到最新数组。
无冗余空间:写操作按需创建新数组,数组长度始终等于元素个数,没有 ArrayList 那样的扩容冗余空间,内存利用率更高。
四、核心操作:读无锁,写复制
CopyOnWriteArrayList 的所有核心操作都围绕“读无锁,写复制”展开。读操作极致简洁,写操作则包含完整的“加锁-复制-修改-替换-解锁”流程。下面拆解最常用的读、增、删、改操作。
1. 读操作:get(int index)——无锁无阻塞
CopyOnWriteArrayList 的读操作是所有并发集合中效率最高的,原因是没有任何同步机制、没有任何校验,直接访问数组,和 ArrayList 几乎完全一样,时间复杂度 O(1)。
(1) 核心源码:
public E get(int index) {
// 仅两步核心操作
return elementAt(getArray(), index);
}
// 私有工具方法:直接执行 a[index]
private E elementAt(Object[] a, int index) {
return (E) a[index];
}(2) 关键细节:
无锁无同步:全程没有 synchronized 或 ReentrantLock,没有锁竞争和上下文切换开销。
无下标校验:源码中未做 index >= len 或 index < 0 的判断,完全依赖 JVM 对数组的原生越界检查。
无 modCount 校验:因为底层数组只读不可变,写操作从不会修改原数组,读操作无需担心数据错乱。
直接获取数组:通过 getArray() 获取 volatile 修饰的底层数组,保证引用可见性。
直接访问元素:通过 elementAt() 直接执行 a[index],是最底层的高效内存操作。
(3) 唯一的“小缺陷”:弱一致性。如果写操作正在执行(已复制新数组但尚未替换引用),读操作访问到的仍是原数组。写操作完成后,读线程才能看到最新数据。不过,在读多写少场景下,这个问题几乎可以忽略不计——写操作频率低,完成速度快,读线程只会在一瞬间读到旧数据,最终都会看到最新值。
2. 写操作:add(E e)——加锁 + 复制数组
写操作是性能开销的核心,但也是保证线程安全的关键。所有写操作(add/remove/set)的核心流程一致:加锁 → 复制原数组 → 在新数组上修改 → 替换引用 → 解锁,时间复杂度 O(n)。
以尾部添加 add(E e) 为例:
public boolean add(E e) {
// 1. 获取独占锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 2. 获取当前原数组
Object[] elements = getArray();
int len = elements.length;
// 3. 复制原数组,创建新数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 4. 在新数组尾部添加元素
newElements[len] = e;
// 5. 替换数组引用
setArray(newElements);
return true;
} finally {
// 6. 解锁
lock.unlock();
}
}关键细节:
独占锁使用 ReentrantLock,支持可重入、公平/非公平选择,比 synchronized 更灵活。
原数组只读:所有写操作都在新数组上执行,确保读操作的安全性。
数组拷贝由 System.arraycopy 完成,这是一个本地方法,具有原子性。
引用替换原子性:volatile 修饰的数组引用赋值是原子的,读线程要么看到旧数组,要么看到新数组,不会看到中间状态。
3. 其他写操作:set/remove——核心流程一致
无论是修改操作 set(int index, E element) 还是删除操作 remove(int index),核心流程与 add 完全一致。仅在修改细节上有所不同。例如,set() 方法会判断新值与旧值是否相同,如果相同则跳过数组拷贝,直接重置引用,以减少不必要的开销。
所有写操作的共性:全程加锁保证线程安全;从不修改原数组,只在新数组上操作以实现读写分离;核心开销是数组拷贝;volatile 数组引用的赋值是原子操作,保证可见性。
五、核心对比:与同类集合的差异
为了在实战中精准选型,我们把 CopyOnWriteArrayList 与 ArrayList、SynchronizedList、Vector 进行全维度对比。
底层实现上,ArrayList 是 Object[] 动态数组,SynchronizedList 和 Vector 也都是 Object[],但 CopyOnWriteArrayList 在此基础上增加了 volatile 修饰与 ReentrantLock。线程安全方面,ArrayList 非线程安全,其他三者均为线程安全。锁粒度上,SynchronizedList 和 Vector 都是方法级同步锁(读写互斥),而 CopyOnWriteArrayList 读操作无锁,写操作使用 ReentrantLock。性能表现上,读多写少场景下 CopyOnWriteArrayList 读性能极致高效,写性能则因数组拷贝相对较弱。迭代表现方面,ArrayList 和 Vector 在迭代过程中若外部修改会触发 ConcurrentModificationException,CopyOnWriteArrayList 则不会。适用场景上,CopyOnWriteArrayList 是读多写少、最终一致性可接受的场景;SynchronizedList 是读写均衡、对一致性敏感的场景;Vector 目前基本被视为遗留类,不推荐在大多数新场景中使用。
六、实战避坑:4 个高频坑点
CopyOnWriteArrayList 虽好,但并非万能。一旦用错场景,性能暴跌、内存溢出、数据一致性问题都可能找上门。以下是 4 个最常见的坑点。
1. 高频写场景,导致性能瓶颈 + 内存溢出
错误场景:用 CopyOnWriteArrayList 处理每秒上千次增删改的高频写操作,例如实时订单列表或消息队列。
核心问题:每次写操作都要加锁并完整拷贝数组。高频写会导致锁竞争激烈,数组拷贝的 O(n) 开销拖累性能,同时 JVM 中同时存在多个数组副本,极易触发内存溢出。
正确方案:高频写 + 高频读场景,选择 ConcurrentLinkedQueue/ConcurrentLinkedDeque(基于链表,CAS 无锁);高频写 + 需要有序存储,选择 ConcurrentSkipListMap/ConcurrentSkipListSet;若必须用 List,则使用 Collections.synchronizedList——虽然读写互斥,但写操作没有数组拷贝开销。
2. 依赖强一致性的场景,导致数据不一致
错误场景:用 CopyOnWriteArrayList 存储金融交易数据、实时库存等需要强一致性的数据。
核心问题:CopyOnWriteArrayList 是最终一致性,读可能读到旧数据,在强一致性要求下会导致业务逻辑错误(如库存超卖)。
正确方案:选择 Collections.synchronizedList 或 Vector(保证强一致性),或者自定义 List 结合 ReentrantReadWriteLock。
3. 存储超大对象或海量数据,写操作时内存暴涨
错误场景:用 CopyOnWriteArrayList 存储百万级超大对象(如每个对象超过 1MB),即使写操作频率不高。
核心问题:写操作会复制整个数组。百万级超大对象的数组拷贝,一次操作就能占用数 GB 内存,触发 GC 甚至 OOM。
正确方案:采用数据分段存储,将海量数据拆分为多个小的 CopyOnWriteArrayList;超大对象场景选择 ConcurrentHashMap;或使用本地缓存 + 分布式缓存组合。
4. 误解遍历中写操作会触发异常
错误认知:认为 CopyOnWriteArrayList 和 ArrayList 一样,遍历中执行写操作会触发 ConcurrentModificationException。
真相:CopyOnWriteArrayList 在遍历中执行写操作不会触发任何异常。因为遍历基于原数组副本,写操作在新数组上执行,互不干扰。不过遍历中看到的是旧数据,写操作的结果要等遍历结束后重新获取才能呈现。
七、实战选型:适用场景与禁用场景
基于设计特性和坑点,下面给出精准的适用与禁用场景。
1. 适用场景(读多写少,最终一致性即可)
缓存场景:本地缓存、热点数据缓存(如用户信息、商品信息),读操作占 99%,写操作仅在数据更新时执行。
配置中心:系统配置、业务配置本地存储,配置很少修改,业务代码高频读取。
静态列表:省份城市、行业分类、字典表等几乎不修改的数据,高频查询。
消息订阅/发布:订阅列表,订阅者高频查询,发布者仅在新增/删除订阅时写。
日志收集:低频率的日志添加,高频率的日志查询/遍历。
2. 禁用场景(红线)
高并发写场景:实时订单、实时消息、库存变更等,写操作频繁,数组拷贝和锁竞争会导致性能暴跌。
强一致性要求场景:金融交易、资金结算、库存管理等,必须立即读到最新数据,最终一致性无法满足。
海量数据/超大对象场景:百万级以上数据或超大对象,写操作时的数组拷贝会导致内存暴涨。
频繁遍历写场景:虽不触发异常,但写结果无法在遍历中及时体现,若业务依赖遍历实时性,会导致逻辑错误。
八、写在最后:写时复制的设计思想,比集合本身更重要
CopyOnWriteArrayList 之所以能成为高并发读场景的性能王者,核心不在于代码有多复杂,而在于背后的写时复制设计思想:读共享,写复制,空间换时间,读写分离。
这种设计思想不仅属于 CopyOnWriteArrayList,它同样贯穿于分布式系统、数据库和操作系统中。Redis 的 RDB 持久化、MySQL InnoDB 的 MVCC、Linux 系统的 fork 进程创建,本质上都是写时复制思想的体现。
对开发者而言,拆解 CopyOnWriteArrayList 的源码,不只是为了弄懂一个集合类的使用方法,更是为了掌握一种解决高并发问题的设计思路。当再次遇到类似场景时,可以跳出“加锁同步”的固有思维,从读写分离、空间换时间的角度,找到更优雅、更高效的解决方案。
技术选型的本质,从来不是选择最先进的技术,而是选择最适配业务场景的技术。CopyOnWriteArrayList 并不完美,但在属于它的场景里,它就是最优解。
游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。
同类文章
Meta 悄悄推出全新社交 AI 应用 Pocket,用 Vibe Coding 生成小程序 / 小游戏分享给他人
7 月 5 日消息,据外媒 Business Insider 报道,Meta 近日悄悄推出了一款名为 Pocket 的社交 AI 应用,相对于一般 AI 平台主要分享 AI 图片 影音内容,这款 Pocket 应用最大的特色是可以自由创建分享 AI 生成(Vibe Coding)的小程序 小
雷克沙JumpDrive A50V 行车记录存储新标杆
雷克沙推出JumpDriveA50V行车记录U盘,采用USB3 2Gen1高速接口,读取速度200MB s,写入最高70MB s,支持哨兵模式与多路同时录像,耐温范围-40℃至85℃,适配特斯拉、理想等新能源车型,已被理想选为原厂标配存储设备。
创邻科技灵机一体机:单CPU驱动千亿模型,开启普惠AI新时代
创邻科技推出「灵机」一体机,单颗主流CPU即可驱动百亿至千亿参数大模型,无需GPU、超频与水冷。搭载RAG和GraphoraX智能体平台,支持150万tokens上下文,性能超越人类阅读速度。以极低成本实现本地化部署,面向中小企业、政府、教育及金融等行业,开启普惠AI算力时代。
苹果液态玻璃设计遭抵制:用户批干扰视线,呼吁立即整改
苹果液态玻璃设计引发用户强烈抵制,被批干扰视线、影响阅读。测试版存在通知文字不清、控制中心视觉混乱等问题。苹果可能调整,该设计面向未来AR眼镜等产品,部分用户也有积极反馈。
时空壶W4Pro凭何成为AI同传行业标杆与引领者
时空壶W4Pro基于BabelOS系统实现矢量降噪与双向同传,将响应和翻译延迟标准提升。2024年营收破两亿元,出口量年增长超100%,获亚马逊翻译机最佳销量,在商务、教育、文旅场景中落地,推动AI同传从概念变为现实。
- 热门数据榜
1
2
3
4
5
6
7
8
9
10
相关攻略
2026-07-16 14:49
2026-07-15 22:01
2026-07-15 22:01
2026-07-15 22:01
2026-07-15 22:01
2026-07-15 22:00
2026-07-15 22:00
2026-07-15 22:00
热门教程
- 游戏攻略
- 安卓教程
- 苹果教程
- 电脑教程

