探索
遍历arraylist(JUC 工具之 CopyOnWriteArrayList 详解)

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 的最佳舞台。


顶一下()     踩一下()

热门推荐

发表评论
0评