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