copyonwritearrayset 的迭代器是 fail-safe 的,因其采用写时复制机制:迭代时持有一份底层数组的不可变快照,读写解耦,不抛 concurrentmodificationexception 且无需加锁。

CopyOnWriteArraySet 的迭代器是 fail-safe 的,不是因为用了锁,而是靠“写时复制”(Copy-On-Write)机制实现无锁遍历——它在迭代开始时就对底层数组做了一次快照,后续所有读操作都基于这个不可变副本,写操作则新建数组并原子替换引用。
底层用 CopyOnWriteArrayList 实现
CopyOnWriteArraySet 内部不直接管理元素数组,而是包装了一个 CopyOnWriteArrayList。它的 add/remove/contains 等方法最终都委托给这个 list。而该 list 的核心特性就是:每次修改(add、remove、set)都会创建新数组,复制旧数据 + 修改后,再用 volatile 引用原子更新。
- 迭代器构造时,直接持有当前数组的引用(即那一刻的快照),不会随后续写操作变化
- 因此遍历时哪怕其他线程正在 add 或 remove,也不会抛 ConcurrentModificationException
- 也不需要加锁——读和写完全解耦,读走快照,写走新数组
迭代器的 fail-safe 行为怎么体现
调用 iterator() 返回的是 COWIterator,它内部保存了创建时刻的数组引用和当前下标。整个遍历过程只读这个数组,不访问原始集合对象的最新状态。
Java项目代码review工具。分析Git变更+完整调用链路上下文,推断业务需求,进行多维度评分和分类汇总,生成完整PRD文档。包含细粒度Java代码审查清单(Null安全、异常处理、Streams、并发、equals/hashCode、资源管理、API设计、性能、MyBatis/ORM、事务边界、SQL/DD...
- 即使中途有线程调用 add(),原数组被替换,COWIterator 仍遍历旧数组,看不到新增元素
- 如果中途有 remove() 删除了某个元素,只要该元素在快照数组里还存在,迭代器仍会返回它
- 没有同步块、没有 ReentrantLock、也没有 CAS 循环重试——纯粹靠不可变快照保证线程安全
为什么能无锁?关键在 volatile + 不可变快照
CopyOnWriteArrayList 的数组引用被声明为 volatile:private transient volatile Object[] array;。每次写操作完成新数组构建后,通过 setArray() 原子更新该引用。
- volatile 保证了新数组对所有线程立即可见,且禁止指令重排序,确保数组内容已初始化完毕才发布引用
- 迭代器拿到的是一个确定长度、确定内容的数组,全程只读,自然无需加锁
- 写操作成本高(复制+重建),但读操作零开销——这正是无锁遍历的代价交换
适用场景与注意事项
它适合读多写少、允许弱一致性(迭代结果不反映实时状态)的场景,比如监听器列表、配置白名单等。
- 不要在迭代过程中依赖“是否包含某新元素”,因为快照里没有
- 避免在高频写场景使用,否则内存和 GC 压力显著上升
- size() 返回的是当前快照长度,但注意:它和迭代器看到的元素数量一定一致,因为 size 也是基于同一数组引用读取的
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










