CopyOnWriteArrayList是 Java 并发包(java.util.concurrent)中提供的一个线程安全的变体List,它通过“写时复制”(Copy-On-Write, COW)机制来实现线程安全。适用于读多写少的并发场景。
一、基本概念
1.什么是 Copy-On-Write?
“写时复制”是一种优化策略:当有多个调用者同时读取同一份资源时,它们共享该资源;只有在某个调用者试图修改资源时,系统才会复制一份副本供其修改,而原始资源保持不变。这样可以避免读写冲突,提高并发性能。
2.适用场景
- 读操作远远多于写操作(如监听器列表、配置项缓存等)。
- 对一致性要求不是强一致(允许短暂的“脏读”),因为读操作看到的是写操作发生前的快照。
- 写操作频率低,且写操作开销可接受(因为每次写都要复制整个数组)。
二、类结构与继承关系
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable- 实现了List接口,支持随机访问(RandomAccess)。
- 支持克隆(Cloneable)和序列化(Serializable)。
- 不继承AbstractList,而是自己完整实现了List接口。
三、核心数据结构
内部使用一个volatile 修饰的 Object 数组存储元素:
ounter(lineprivate transient volatile Object[] array;- volatile保证了数组引用的可见性:一个线程修改 array 后,其他线程能立即看到新数组。
- 所有写操作都会创建一个新数组,替换旧数组引用。
- 读操作直接访问当前 array,无需加锁。
四、线程安全机制
1.读操作(无锁)
- 所有读方法(如get(int),size(),iterator()等)直接读取array。
- 因为 array 是 volatile,所以读到的是最新发布的数组(但不一定是“最新写入”的,因为写可能还在进行中)。
- 读操作不会阻塞写操作,写操作也不会阻塞读操作。
2.写操作(加锁 + 复制)
- 所有修改方法(如 add, set, remove)都使用 ReentrantLock 加锁。
- 流程:
- 获取锁。
- 复制原数组(Arrays.copyOf)。
- 在新数组上执行修改。
- 将 array 引用指向新数组。
- 释放锁。
示例(简化版add(E)):
public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); }}迭代器(Iterator)特性
1.“弱一致性”迭代器
- CopyOnWriteArrayList的迭代器是fail-safe(而非 fail-fast)。
- 迭代器在创建时拷贝了一份 array 快照,后续对原 list 的修改不会反映到迭代器中。
- 不会抛出ConcurrentModificationException。
2.不支持 remove/set/add
- 迭代器的remove(),set(),add()方法会抛出UnsupportedOperationException。
- 因为迭代器操作的是快照,修改快照没有意义。
示例:
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();list.add("A");list.add("B");for (String s : list) { list.add("C"); // 安全!不会影响当前迭代 System.out.println(s); // 只会打印 A, B}性能分析
操作 | 时间复杂度 | 说明 |
get(i) | O(1) | 直接数组访问 |
add(e) | O(n) | 需要复制整个数组 |
remove(i) | O(n) | 复制 + 移动元素 |
iterator() | O(1) | 返回快照引用(实际复制发生在写时) |
优点:
- 读操作高性能、无锁。
- 迭代时不抛ConcurrentModificationException。
- 线程安全,无需外部同步。
缺点:
- 写操作开销大(内存 + CPU)。
- 内存占用高(写时同时存在新旧两个数组)。
- 数据“最终一致性”:读可能看不到最新写入(取决于何时获取 array 引用)。
与其他集合对比
特性 | ArrayList | Vector | Collections.synchronizedList | CopyOnWriteArrayList |
线程安全 | ❌ | ✅(synchronized) | ✅(synchronized) | ✅(COW) |
读性能 | 高 | 低(加锁) | 低(加锁) | 极高(无锁) |
写性能 | 高 | 低 | 低 | 极低(复制数组) |
迭代器 | fail-fast | fail-fast | fail-fast | fail-safe |
适用场景 | 单线程 | 老式同步 | 通用同步 | 读多写少 |
典型场景应用
1.事件/监听器列表
多个线程注册监听器,主线程广播事件(大量读,偶尔添加监听器)。
2.配置信息缓存
配置很少变更,但被频繁读取。
3.白名单/黑名单管理
如 IP 白名单,更新频率低,但每次请求都要检查。
注意事项与陷阱
1.内存敏感场景慎用
每次写操作都会导致双倍内存占用(新旧数组共存),可能引发 OOM。
2.不适合实时性要求高的场景
读操作可能看到“过期”数据(例如刚 add 一个元素,另一个线程立即 get 可能看到旧值)。
3.避免在写密集场景使用
频繁写会导致大量数组复制,性能急剧下降。
4.不支持批量高效写
没有像addAll那样优化的批量写(虽然有addAll,但内部仍是逐个复制+合并)。
源码关键点(JDK 17+)
- 使用ReentrantLock而非synchronized,更灵活。
- getArray()和setArray()是封装方法,便于 future 扩展。
- 所有写方法都遵循“锁 → 复制 → 修改 → 替换 → 解锁”模式。
- 序列化时只写入当前 array 内容,反序列化时重建 volatile array。
替代方案建议
- 如果写操作较多→ 考虑Collections.synchronizedList(new ArrayList<>())+ 手动同步块控制粒度。
- 如果需要强一致性 + 高并发读写→ 考虑ConcurrentHashMap(如果 key-value 结构合适)或其他并发集合。
- 如果只是避免 CME→ 可考虑先复制再遍历(如new ArrayList<>(list).forEach(...))。
总结
CopyOnWriteArrayList是一种以空间换时间、以最终一致性换高并发读性能的设计典范。它在特定场景下非常高效,但在通用场景或写密集型应用中应谨慎使用。理解其“快照语义”和“写复制开销”是正确使用的关键。
读多写少 + 容忍短暂不一致 = CopyonWriteArrayList 的最佳舞台。
