fail-safe机制是通过创建内存快照实现读写分离的安全迭代,其核心是在iterator()调用时一次性复制集合状态,确保遍历不中断但带来内存开销。

fail-safe 机制不是“迭代失效”,而是**主动规避失效风险的设计选择**:它通过牺牲实时一致性,换取遍历过程不中断、不抛异常。它的核心不在“避免失败”,而在“失败时有安全兜底”——这个兜底,就是内存中的快照拷贝。
fail-safe 的本质是读写分离,不是无代价的“安全”
所谓“安全”,指迭代器不会因并发修改而崩溃;但这个安全是有明确实现成本的:每次创建迭代器,都要生成一份集合状态的只读副本。这不是懒加载或引用共享,而是实实在在的堆内存复制。
- CopyOnWriteArrayList:每次调用 iterator(),内部会调用 getArray() 获取当前数组,并直接返回该数组的引用(注意:是引用,但数组本身不可变);而后续所有 add/remove 操作都会新建数组、复制元素、再原子更新引用 —— 所以迭代器看到的永远是创建那一刻的完整快照
- ConcurrentHashMap 的 keySet().iterator():不全量复制整个 map,但在某些桶(bin)发生扩容或迁移时,可能触发局部结构快照;迭代器遍历的是分段视图(segments 或 node 数组的稳定切片),仍存在隐式复制开销
拷贝发生在哪一刻?不是遍历中,而是 iterator() 调用时
关键细节常被误解:快照不是在 next() 时逐个复制元素,而是在获取迭代器对象的瞬间完成的。例如:
Iterator
list.add("e"); // ← 新建数组,复制全部5个元素,更新 list 内部引用
while (it.hasNext()) System.out.print(it.next()); // 输出 a b c d,看不到 "e"
也就是说,拷贝动作是一次性、不可逆、不可共享的——每个 iterator 实例都持有一份独立快照引用,哪怕内容完全相同。
为什么拷贝会导致 OOM?不是因为“一次复制”,而是“反复+叠加+滞留”
单次快照对小集合几乎无感,但以下模式会让内存压力指数级上升:
- 大集合(如 50 万订单对象)被频繁创建迭代器:每秒调用 10 次 iterator(),等于每秒新增 10 份 50 万对象的引用副本
- 迭代器生命周期长(比如被缓存、传入异步任务、或绑定到 request scope),旧快照无法及时被 GC 回收
- 日志打印触发 toString():log.info("data: {}", list) → CopyOnWriteArrayList.toString() 内部会遍历并拼接,等价于隐式调用一次 iterator()
- 嵌套循环中重复获取迭代器:for (X x : list) { for (Y y : list) { ... } } → 外层一次,内层每次循环都新建一个快照
它和 fail-fast 的根本区别不在“是否报错”,而在“谁承担一致性成本”
fail-fast 把一致性校验压给运行时(modCount 对比),轻量、即时、零拷贝,但一错即停;fail-safe 把一致性让渡给内存(快照隔离),允许继续执行,但把成本提前转嫁为堆空间占用。选哪个,取决于场景优先级:要确定性就选 fail-fast,要可用性且数据量可控才考虑 fail-safe。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











