0%

CopyOnWrite容器

CopyOnWrite容器:读写分离的并发容器设计与实现

CopyOnWrite(写时复制)容器是一种通过读写分离延迟更新策略实现的高效并发容器,核心思想是:读操作直接访问当前容器,写操作则复制一份新容器进行修改,完成后再替换旧容器。这种设计在 “读多写少” 的场景下能显著提升并发性能,避免读写冲突。本文以 CopyOnWriteArrayList 为例,深入解析其原理、实现及适用场景。

CopyOnWrite 核心思想

读写分离

  • 读操作:直接访问当前容器(无锁),无需阻塞;
  • 写操作:不直接修改原容器,而是复制一份新容器进行修改,修改完成后通过原子操作替换旧容器;
  • 最终一致性:写操作期间,读操作仍访问旧容器,避免 “脏读”;写操作完成后,所有新读操作会访问新容器,保证数据最终一致。

适用场景

  • 读多写少:如配置缓存、白名单列表等,读取频率远高于修改频率;
  • 允许数据短暂不一致:写操作完成前,读操作可能访问旧数据,适用于对实时性要求不高的场景;
  • 遍历操作频繁:避免迭代时的 ConcurrentModificationException(传统同步容器在迭代中修改会抛此异常)。

CopyOnWriteArrayList 源码解析

CopyOnWriteArrayList 是 List 接口的并发实现,底层通过数组存储数据,核心源码如下:

核心成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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:写操作时加锁,确保同一时间只有一个线程复制和修改容器,避免多线程写操作导致的副本混乱;
  • arrayvolatile 修饰的数组,保证读操作能立即看到最新的容器替换(写操作完成后 setArray 的可见性)。

写操作:add (E e)

写操作(添加、修改、删除)的核心是复制旧数组→修改新数组→替换旧数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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)

读操作无需加锁,直接访问当前数组,性能极高:

1
2
3
4
5
6
7
8
public E get(int index) {  
// 直接访问当前数组(无锁)
return get(getArray(), index);
}

private E get(Object[] a, int index) {
return (E) a[index];
}

特点

  • 读操作无锁,无需等待,适合高并发读取场景;
  • 可能读取到旧数据(若写操作正在进行),但不会读到 “部分修改” 的中间状态(因写操作在新数组中完成)。

遍历操作:iterator ()

CopyOnWriteArrayList 的迭代器基于快照实现,遍历的是迭代器创建时的数组副本,避免 ConcurrentModificationException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 实现,核心是利用其 “添加前检查是否存在” 的特性保证元素唯一性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 共享相同的并发特性(读无锁、写复制);
  • 适合 “读多写少” 且需要去重的场景(如用户标签集合)。

优缺点分析

优点

  1. 高并发读性能优异:读操作无锁,无需阻塞,适合读多写少场景;
  2. 迭代安全:迭代器基于快照,不会抛出 ConcurrentModificationException
  3. 最终一致性:写操作不影响读操作,避免 “脏读” 和部分修改的中间状态。

缺点

  1. 内存占用翻倍:写操作时复制整个数组,若数组较大,会占用双倍内存;
  2. 写操作性能差:复制数组耗时(O (n) 时间复杂度),不适合频繁修改的场景;
  3. 数据实时性低:写操作完成前,读操作访问旧数据,可能导致短暂的数据不一致。

与其他并发容器的对比

容器类型 核心机制 读性能 写性能 内存占用 适用场景
CopyOnWriteArrayList 写时复制,读写分离 极高 较差 读多写少,允许短暂不一致
Vector 全量加锁(synchronized) 读写频率均衡,需强一致性
ConcurrentHashMap 分段锁,CAS 键值对存储,高并发读写

最佳实践

  1. 场景选择:仅在 “读操作远多于写操作” 且 “可接受数据短暂不一致” 时使用(如配置缓存、静态列表);
  2. 避免大容量容器:数组过大时,复制操作会导致内存飙升和 GC 压力;
  3. 减少写操作频率:批量修改时尽量合并操作(如先收集所有修改,再一次性执行 addAll);
  4. 不依赖实时性:若业务要求 “写后立即可见”,需使用 ReentrantReadWriteLock 等强一致性方案。

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10