copyonwritearraylist 的 fail-safe 遍历靠写时复制数组实现:写操作加锁、复制数组、原子更新 volatile 引用;迭代器持旧快照,不抛 concurrentmodificationexception,但无法感知后续修改,适合读多写少场景。

CopyOnWriteArrayList 的 fail-safe 遍历不是靠锁或版本号实现的,而是靠每次写操作时复制整个底层数组,让迭代器始终持有一个不可变的快照。
写操作触发数组复制,生成新快照
当调用 add、remove、set 等修改方法时,CopyOnWriteArrayList 会先加锁(ReentrantLock),然后创建当前数组的完整副本,在副本上完成修改,再用原子方式将 volatile 数组引用指向新副本。原数组不受影响,正在遍历的迭代器仍持有旧数组引用。
- 复制开销随数组大小线性增长,适合读多写少场景
- 写操作期间其他线程的读操作不受阻塞,仍可访问旧快照
- 多个并发写操作会串行执行,每次生成独立快照
迭代器基于构造时刻的数组快照运行
调用 iterator() 时,迭代器直接保存当前 volatile array 的引用(即那一刻的数组副本),后续所有 next()、hasNext() 都在这个固定数组上进行,不检查原始列表是否变更,也不与最新数组同步。
Java JDK 25 来自 OpenJDK 官方归档,版本为 JDK 25,本条下载地址已指向官方 Windows x64 zip 安装包直链,适合调试旧项目或兼容旧版 Java 运行环境。
- 不会抛出 ConcurrentModificationException,天然 fail-safe
- 无法看到写操作在迭代开始后发生的任何变更(包括新增、删除、修改)
- 即使迭代器还在运行,原列表已更新多次,它仍只遍历初始快照
内存可见性由 volatile 数组引用保障
底层数组引用被声明为 volatile,确保每次写操作完成后,新数组地址对所有线程立即可见;同时保证迭代器读取该引用时能获得最新已发布的快照地址——但仅限于它自己获取那一刻的值,之后不再刷新。
- volatile 不保证数组元素本身的可见性,但因每次写都新建数组且元素全量拷贝,所以快照内数据天然一致
- 迭代器不感知后续 volatile 引用的更新,这是设计使然,不是 bug
- 如果需实时一致性,应换用 ConcurrentHashMap 或加锁同步访问
典型误用与边界情况
开发者有时误以为迭代器能“看到部分更新”,或在遍历时依赖 size() 返回值做逻辑判断,但 size() 返回的是当前最新数组长度,而迭代器遍历的是旧快照,二者可能不一致。
- 遍历中调用 list.size() 得到的是最新长度,但 iterator.hasNext() 基于旧数组长度
- 不能在遍历中调用 remove() —— 迭代器的 remove() 是空实现,不支持
- 大量写操作会导致频繁数组复制和内存压力,GC 压力上升
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










