CopyOnWrite容器:读写分离的并发容器设计与实现
CopyOnWrite(写时复制)容器是一种通过读写分离和延迟更新策略实现的高效并发容器,核心思想是:读操作直接访问当前容器,写操作则复制一份新容器进行修改,完成后再替换旧容器。这种设计在 “读多写少” 的场景下能显著提升并发性能,避免读写冲突。本文以 CopyOnWriteArrayList 为例,深入解析其原理、实现及适用场景。
CopyOnWrite 核心思想
读写分离
- 读操作:直接访问当前容器(无锁),无需阻塞;
- 写操作:不直接修改原容器,而是复制一份新容器进行修改,修改完成后通过原子操作替换旧容器;
- 最终一致性:写操作期间,读操作仍访问旧容器,避免 “脏读”;写操作完成后,所有新读操作会访问新容器,保证数据最终一致。
适用场景
- 读多写少:如配置缓存、白名单列表等,读取频率远高于修改频率;
- 允许数据短暂不一致:写操作完成前,读操作可能访问旧数据,适用于对实时性要求不高的场景;
- 遍历操作频繁:避免迭代时的
ConcurrentModificationException(传统同步容器在迭代中修改会抛此异常)。
CopyOnWriteArrayList 源码解析
CopyOnWriteArrayList 是 List 接口的并发实现,底层通过数组存储数据,核心源码如下:
核心成员变量
1 | public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { |
lock:写操作时加锁,确保同一时间只有一个线程复制和修改容器,避免多线程写操作导致的副本混乱;array:volatile修饰的数组,保证读操作能立即看到最新的容器替换(写操作完成后setArray的可见性)。
写操作:add (E e)
写操作(添加、修改、删除)的核心是复制旧数组→修改新数组→替换旧数组:
1 | public boolean add(E e) { |
流程解析:
- 加锁确保写操作独占,避免多线程同时复制数组导致的资源浪费和数据不一致;
- 复制旧数组时,新数组与旧数组是完全独立的内存空间,读操作可继续安全访问旧数组;
setArray是原子操作,替换瞬间所有新读操作会访问新数组,实现 “最终一致性”。
读操作:get (int index)
读操作无需加锁,直接访问当前数组,性能极高:
1 | public E get(int index) { |
特点:
- 读操作无锁,无需等待,适合高并发读取场景;
- 可能读取到旧数据(若写操作正在进行),但不会读到 “部分修改” 的中间状态(因写操作在新数组中完成)。
遍历操作:iterator ()
CopyOnWriteArrayList 的迭代器基于快照实现,遍历的是迭代器创建时的数组副本,避免 ConcurrentModificationException:
1 | public Iterator<E> iterator() { |
优势:
- 迭代期间,即使原容器被修改,迭代器仍访问快照数组,不会抛出
ConcurrentModificationException; - 适合需要频繁遍历且修改较少的场景(如日志查询、数据统计)。
CopyOnWriteArraySet:基于 CopyOnWriteArrayList 的 Set 实现
CopyOnWriteArraySet 内部通过 CopyOnWriteArrayList 实现,核心是利用其 “添加前检查是否存在” 的特性保证元素唯一性:
1 | public class CopyOnWriteArraySet<E> extends AbstractSet<E> implements Serializable { |
特点:
- 与
CopyOnWriteArrayList共享相同的并发特性(读无锁、写复制); - 适合 “读多写少” 且需要去重的场景(如用户标签集合)。
优缺点分析
优点
- 高并发读性能优异:读操作无锁,无需阻塞,适合读多写少场景;
- 迭代安全:迭代器基于快照,不会抛出
ConcurrentModificationException; - 最终一致性:写操作不影响读操作,避免 “脏读” 和部分修改的中间状态。
缺点
- 内存占用翻倍:写操作时复制整个数组,若数组较大,会占用双倍内存;
- 写操作性能差:复制数组耗时(O (n) 时间复杂度),不适合频繁修改的场景;
- 数据实时性低:写操作完成前,读操作访问旧数据,可能导致短暂的数据不一致。
与其他并发容器的对比
| 容器类型 | 核心机制 | 读性能 | 写性能 | 内存占用 | 适用场景 |
|---|---|---|---|---|---|
CopyOnWriteArrayList |
写时复制,读写分离 | 极高 | 较差 | 高 | 读多写少,允许短暂不一致 |
Vector |
全量加锁(synchronized) | 低 | 低 | 低 | 读写频率均衡,需强一致性 |
ConcurrentHashMap |
分段锁,CAS | 高 | 中 | 中 | 键值对存储,高并发读写 |
最佳实践
- 场景选择:仅在 “读操作远多于写操作” 且 “可接受数据短暂不一致” 时使用(如配置缓存、静态列表);
- 避免大容量容器:数组过大时,复制操作会导致内存飙升和 GC 压力;
- 减少写操作频率:批量修改时尽量合并操作(如先收集所有修改,再一次性执行
addAll); - 不依赖实时性:若业务要求 “写后立即可见”,需使用
ReentrantReadWriteLock等强一致性方案。
v1.3.10