Iterator 的 fail-fast 和 fail-safe 是什么?
简化版
fail-fast 指迭代过程中检测到集合被结构性修改时快速抛出 ConcurrentModificationException;fail-safe 指迭代时基于快照或弱一致性机制,不直接抛这个异常。ArrayList 常见迭代器是 fail-fast,CopyOnWriteArrayList 的迭代器更接近 fail-safe。
详细版
fail-fast 是一种快速失败机制。以 ArrayList 为例,集合维护 modCount,迭代器创建时保存 expectedModCount。遍历过程中如果发现两者不一致,说明集合被非迭代器方式结构性修改,就抛出 ConcurrentModificationException。
fail-safe 通常不会直接遍历原集合的可变结构。例如 CopyOnWriteArrayList 迭代器基于创建时的数组快照,遍历时即使原集合修改,也不影响当前迭代器。
需要注意:fail-fast 不是线程安全保证,只是一种尽早暴露并发修改风险的检测机制。
完整版教学
一、什么是结构性修改
结构性修改通常指会改变集合大小或内部结构的操作,比如:
- 添加元素。
- 删除元素。
- 清空集合。
单纯修改已有元素的字段,通常不算集合结构性修改。
例如:
for (String s : list) {
list.remove(s);
}
这种写法容易触发 fail-fast,因为增强 for 背后使用迭代器,而循环内部直接修改了集合。
二、fail-fast 的基本原理
很多集合类中有一个修改计数 modCount。创建迭代器时,迭代器会记录当时的值。
遍历时,迭代器会检查:
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
如果集合被外部修改,modCount 变了,而迭代器的 expectedModCount 没变,就会快速失败。
三、为什么 Iterator.remove 可以删除
用迭代器自己的 remove() 删除元素通常是允许的。
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if (s.startsWith("A")) {
it.remove();
}
}
迭代器删除后,会同步更新自己的期望修改次数,使 expectedModCount 和集合 modCount 保持一致。
这也是面试里经常追问的点:遍历时删除元素,不要直接用集合的 remove(),要用迭代器的 remove()。
四、fail-safe 是怎么回事
fail-safe 不是 Java 标准接口里的正式强制术语,但常用来描述“不因并发修改直接失败”的迭代方式。
典型例子是 CopyOnWriteArrayList。它写入时复制新数组,迭代器持有创建时的数组快照。
因此:
- 遍历过程中不会抛
ConcurrentModificationException。 - 当前迭代器看不到创建之后的新修改。
- 读多写少场景比较适合。
五、弱一致性迭代器
并发集合如 ConcurrentHashMap 的迭代器通常是弱一致性的。它不会抛 ConcurrentModificationException,遍历时可能看到部分并发修改,也可能看不到。
它不保证遍历结果是某个严格时刻的完整快照,但能在并发环境下安全遍历。
六、fail-fast 不是线程安全
很多人会误以为 fail-fast 可以保证并发安全。其实它只是检测到异常修改时尽快报错。
在并发环境下,它不能保证一定抛异常,也不能保证数据一致。真正需要线程安全时,应使用同步控制或并发集合。
七、常见误区与追问
fail-fast 是尽早暴露非预期结构修改的最佳努力机制,不是线程安全保证。ArrayList 迭代器保存 expectedModCount,若遍历期间集合通过其他路径结构性修改,使它与 modCount 不同,就可能抛 ConcurrentModificationException。CopyOnWriteArrayList 使用创建迭代器时的数组快照,因此遍历看不到之后新增的第4个元素。
| 检查维度 | 判定依据 |
|---|---|
| fail-fast | 检测到版本变化后尽快失败 |
| 快照/弱一致迭代 | 允许并发变化,但可见性语义不同 |
expectedModCount != modCount -> ConcurrentModificationException
记忆钩子:fail-fast 是 bug 探测器,不是并发锁。
- 误区:没有抛异常就证明没有并发修改。 检测是最佳努力,不能作为程序正确性的同步手段。
- 追问:迭代器自身 remove 为什么通常不触发? 它会同步更新集合结构与 expectedModCount。
- 误区:fail-safe 是 JDK Iterator 的正式统一术语。 更准确应说明快照迭代或弱一致迭代的具体容器语义。
- 追问:CopyOnWrite 适合写多场景吗? 不适合,每次写会复制数组,适合读多写少。
- 追问:ConcurrentHashMap 迭代会看到什么? 它是弱一致的,可能反映遍历期间的部分更新且不抛 CME。
八、加强记忆
fail-fast 是“发现被外部结构性修改就尽快报错”,fail-safe 是“通过快照或弱一致性避免直接报错”。前者帮助暴露错误用法,后者适合特定并发读场景,但都不能随便等同于绝对安全。