CopyOnWriteArrayList 源码分析
CopyOnWriteArrayList 是 Java 并发包(java.util.concurrent)提供的线程安全 List 实现,核心设计思想是 "读写分离 + 写时复制" ,既能保证线程安全,又能避免读写互斥导致的性能损耗,适用于 读多写少 的并发场景。
一、核心特性概述
- 线程安全:读操作无锁,写操作加独占锁,避免并发修改问题;
- 写时复制(Copy-On-Write):写操作(add/remove/set)不会直接修改原数组,而是复制一份新数组进行修改,修改完成后替换原数组引用;
- 弱一致性 :读操作(get / 迭代)基于原数组快照,可能读取到 "旧数据",但不会抛出
ConcurrentModificationException; - 无快速失败(fail-fast):迭代器遍历期间集合被修改,不会触发快速失败机制。
二、类结构与核心成员
2.1 类继承与实现
java
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
// ...
}
- 实现
List接口:具备 List 集合的基本特性(有序、可重复); - 实现
RandomAccess接口:支持随机访问(通过索引快速获取元素); - 实现
Cloneable/Serializable接口:支持克隆和序列化。
2.2 核心成员变量
java
// 存储元素的核心数组(volatile 保证读操作可见性)
private transient volatile Object[] array;
// 写操作的独占锁(保证写操作互斥)
private final transient ReentrantLock lock = new ReentrantLock();
array:用volatile修饰,确保当数组被替换时,所有读线程能立即看到最新的数组引用;lock:ReentrantLock独占锁,保证同一时间只有一个写操作执行(避免多线程同时复制数组导致数据不一致)。
三、构造方法
CopyOnWriteArrayList 提供 3 个核心构造方法(JDK 8),核心是初始化 array 数组:
3.1 无参构造(默认初始化)
java
public CopyOnWriteArrayList() {
// 初始化空数组(不可变空数组常量)
setArray(new Object[0]);
}
// 封装 array 的赋值操作(私有方法)
private void setArray(Object[] a) {
array = a;
}
3.2 基于集合初始化
java
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
// 优化:如果传入的是 CopyOnWriteArrayList,直接复用其 array
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
// 否则转为数组(toArray() 可能返回非 Object[] 类型,需拷贝)
elements = c.toArray();
// c.toArray() 可能返回的是 c 自身的内部数组,需复制一份避免外部修改
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
3.3 基于数组初始化(包访问权限)
java
// 仅用于内部或同包调用,直接使用传入的数组(不复制,需确保数组不可变)
CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
四、核心方法解析
4.1 读操作(无锁,高效)
读操作(get/contains/indexOf)无需加锁,直接基于当前 array 快照访问,性能极高。
4.1.1 get 方法(按索引获取元素)
java
public E get(int index) {
// 1. 获取当前 array 快照(volatile 保证可见性)
// 2. 直接通过索引访问(RandomAccess 特性)
return get(getArray(), index);
}
// 封装 array 的获取操作(私有方法)
private Object[] getArray() {
return array;
}
// 实际获取元素(无锁,直接访问数组)
private E get(Object[] a, int index) {
return (E) a[index];
}
- 关键 :读操作不修改数组,且
array是volatile,所以无需加锁; - 弱一致性 :如果读操作执行时,写操作正在复制新数组但未替换
array,读线程仍会读取旧数组的元素。
4.1.2 contains 方法(判断元素是否存在)
java
public boolean contains(Object o) {
// 获取当前 array 快照,遍历判断
Object[] elements = getArray();
return indexOf(o, elements, 0, elements.length) >= 0;
}
// 私有工具方法:遍历数组查找元素(支持 null 元素)
private static int indexOf(Object o, Object[] elements, int index, int fence) {
if (o == null) {
for (int i = index; i < fence; i++)
if (elements[i] == null)
return i;
} else {
for (int i = index; i < fence; i++)
if (o.equals(elements[i]))
return i;
}
return -1;
}
- 支持
null元素(通过==判断); - 基于快照遍历,不影响写操作。
4.2 写操作(加锁,复制数组)
所有写操作(add/remove/set)都需通过 lock 加锁,确保互斥,核心流程:
- 加锁;
- 获取当前数组快照;
- 复制一份新数组(修改长度或元素);
- 在新数组上执行写操作;
- 替换
array引用为新数组; - 解锁。
4.2.1 add 方法(末尾添加元素)
java
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock(); // 加独占锁,保证写操作互斥
try {
Object[] elements = getArray(); // 获取当前数组
int len = elements.length;
// 复制新数组(长度 +1)
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e; // 新元素添加到末尾
setArray(newElements); // 替换原数组引用
return true;
} finally {
lock.unlock(); // 最终解锁(避免异常导致死锁)
}
}
4.2.2 add (int index, E element) 方法(指定索引添加)
java
public void add(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// 检查索引合法性(index 不能大于 len)
if (index > len || index < 0)
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + len);
Object[] newElements;
int numMoved = len - index;
if (numMoved == 0) {
// 索引 == 长度(末尾添加),直接复制 len+1 长度数组
newElements = Arrays.copyOf(elements, len + 1);
} else {
// 索引在中间:创建 len+1 长度新数组,分两次复制
newElements = new Object[len + 1];
System.arraycopy(elements, 0, newElements, 0, index); // 复制前半部分
System.arraycopy(elements, index, newElements, index + 1, numMoved); // 复制后半部分
}
newElements[index] = element; // 插入新元素
setArray(newElements);
} finally {
lock.unlock();
}
}
4.2.3 remove 方法(删除指定索引元素)
java
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index); // 先获取要删除的旧元素
int numMoved = len - index - 1;
if (numMoved == 0) {
// 删除最后一个元素,复制 len-1 长度数组
setArray(Arrays.copyOf(elements, len - 1));
} else {
// 中间元素删除:创建 len-1 长度新数组,分两次复制(跳过删除元素)
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index, numMoved);
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
4.2.4 set 方法(修改指定索引元素)
java
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
E oldValue = get(elements, index); // 获取旧值
if (oldValue != element) { // 元素不同才修改(优化)
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len); // 复制新数组
newElements[index] = element; // 修改新数组元素
setArray(newElements); // 替换原数组
} else {
// 元素相同,无需修改(避免复制数组,提升性能)
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
- 优化点:如果新元素与旧元素相同,不复制数组,直接复用原数组。
4.3 迭代器(弱一致性)
CopyOnWriteArrayList 的迭代器 COWIterator 是 弱一致性 的,核心特性:
- 迭代器创建时,持有当前
array的快照(snapshot); - 迭代期间集合被修改(替换
array),迭代器仍遍历快照,不会抛出ConcurrentModificationException; - 迭代器不支持
remove操作(会抛UnsupportedOperationException)。
迭代器核心源码
java
private static class COWIterator<E> implements ListIterator<E> {
// 迭代器创建时的数组快照(不可变)
private final Object[] snapshot;
// 当前迭代位置
private int cursor;
// 构造器:传入创建时的 array 快照
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
// 是否有下一个元素(基于快照判断)
public boolean hasNext() {
return cursor < snapshot.length;
}
// 获取下一个元素(基于快照)
@SuppressWarnings("unchecked")
public E next() {
if (!hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
// 不支持 remove 操作
public void remove() {
throw new UnsupportedOperationException();
}
// ... 其他方法(hasPrevious、previous 等,均基于快照)
}
- 弱一致性的原因:快照与原数组解耦,写操作修改的是新数组,不影响快照。
五、并发安全性分析
5.1 读操作安全
- 读操作无锁,依赖
array的volatile特性:确保读线程能立即看到最新的数组引用; - 数组本身是 "不可变" 的(写操作通过复制新数组修改),读线程遍历数组时不会出现 "数组长度变化" 或 "元素被中途修改" 的问题。
5.2 写操作安全
- 写操作通过
ReentrantLock独占锁保证互斥:避免多个写线程同时复制数组,导致数据覆盖; - 写操作修改的是新数组,修改完成后才替换
array引用,不会影响正在进行的读操作。
5.3 弱一致性的本质
- 读操作可能读取到 "旧数据":因为写操作的数组复制和替换需要时间,读线程可能在替换前获取到旧数组;
- 适用于 "最终一致性" 场景,不适用于强一致性要求(如金融交易)。
六、优缺点总结
6.1 优点
- 读性能极高:读操作无锁,支持高并发读;
- 线程安全:无需手动加锁,避免并发修改问题;
- 迭代稳定 :迭代器不会抛出
ConcurrentModificationException,遍历过程平滑。
6.2 缺点
- 写性能差:每次写操作都要复制数组,时间复杂度 O (n),且消耗额外内存(最坏情况下双倍内存);
- 弱一致性:读操作可能获取旧数据,不适合强一致性场景;
- 内存占用高:写操作期间,原数组和新数组同时存在,大数据量场景下内存压力大。
七、适用场景与对比
7.1 适用场景
- 读多写少:如配置中心、缓存列表、静态数据查询(读操作占比 90%+,写操作极少);
- 最终一致性需求:允许读操作短暂获取旧数据,无需强实时性。
7.2 与其他 List 对比
| 特性 | CopyOnWriteArrayList | ArrayList | Vector |
|---|---|---|---|
| 线程安全 | 是(读写分离) | 否 | 是(synchronized) |
| 读性能 | 极高(无锁) | 高(非并发) | 低(读写加锁) |
| 写性能 | 低(复制数组) | 高(非并发) | 低(同步锁) |
| 迭代器特性 | 弱一致性(不抛异常) | 快速失败(抛异常) | 快速失败(抛异常) |
| 内存占用 | 高(写时复制) | 低 | 低 |
八、注意事项
- 元素特性 :支持
null元素,依赖equals方法实现contains/indexOf等操作; - 批量操作优化 :使用
addAll/removeAll等批量方法,避免多次单个写操作(减少数组复制次数); - 避免写频繁场景:如高频添加 / 删除的场景(如秒杀库存列表),会导致严重的内存开销和性能下降;
- 序列化 :
array用transient修饰,通过自定义序列化(writeObject/readObject)保证序列化安全,避免序列化期间数组被修改。
总结
CopyOnWriteArrayList 是 "读多写少" 并发场景的最优解之一,其核心设计 "写时复制" 巧妙平衡了线程安全和读性能。但需注意其弱一致性和写操作的内存 / 性能开销,避免在写频繁或强一致性场景中使用。理解其源码设计(volatile 数组 + 独占锁 + 快照迭代器),能帮助我们更合理地选择并发集合。