什么是 fail-fast?什么是 fail-safe?
结论先行:fail-fast 是“发现并发修改就立刻抛异常”的检测机制:集合内部维护 modCount,迭代器创建时记下 expectedModCount,每次取元素前比对,不一致就抛 ConcurrentModificationException。fail-safe 不是 JDK 的正式术语,通常指弱一致性迭代器或写时复制容器:迭代基于快照或并发结构,不抛 CME,但可能看不到最新修改。
1. 两种策略对比
| 维度 | fail-fast | fail-safe |
|---|---|---|
| 典型容器 | ArrayList、HashMap、HashSet | CopyOnWriteArrayList、ConcurrentHashMap |
| 实现方式 | modCount 校验 | 快照复制 / 弱一致性遍历 |
| 是否抛 CME | 是 | 否 |
| 额外内存 | 无 | 写时复制需要整份快照 |
| 数据一致性 | 立即失败,绝不返回脏数据 | 可能读到旧数据或部分更新 |
| 适用场景 | 单线程,尽早暴露 bug | 并发读多写少 |
2. fail-fast 的源码依据
// ArrayList.Itr:迭代器持有 modCount 的快照
private class Itr implements Iterator<E> {
int cursor; // 下一个元素下标
int lastRet = -1;
int expectedModCount = modCount; // 创建迭代器时记录
public boolean hasNext() {
return cursor != size;
}
public E next() {
checkForComodification(); // 每次取元素前校验
// ...
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
list: [A][B][C] modCount = 3
迭代器创建 expectedModCount = 3
list.remove(B) modCount = 4(结构性修改)
itr.next() 3 != 4 → 抛 ConcurrentModificationException
3. 代码:单线程也会触发
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.List;
public class FailFastDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>(List.of("a", "b", "c", "d"));
// for-each 底层就是迭代器,循环体里直接删元素会抛 CME
try {
for (String s : list) {
if (s.equals("b")) list.remove(s); // 删除成功后继续 next()
}
} catch (ConcurrentModificationException e) {
System.out.println("迭代期间结构性修改,抛 CME");
}
// 正确做法一:iterator.remove(),会同步 expectedModCount
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().equals("c")) it.remove();
}
// 正确做法二:removeIf,内部仍走迭代器
list.removeIf("a"::equals);
System.out.println(list); // 输出:[d]
}
}
注意 list.remove(s) 本身是成功的,CME 发生在下一次 next() 的校验里——异常只是“事后报警”,不会回滚已做的修改。
4. fail-safe 的两种形态
写时复制:CopyOnWriteArrayList 的 add / remove 会复制整个数组再替换引用,迭代器捕获的是旧数组,读操作完全不加锁。
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class FailSafeDemo {
public static void main(String[] args) {
List<String> list = new CopyOnWriteArrayList<>(new String[]{"a", "b"});
for (String s : list) {
if (s.equals("a")) {
list.add("c"); // 迭代的是旧快照,不会抛 CME
}
}
System.out.println(list); // 输出:[a, b, c]
System.out.println(list.size()); // 输出:3
}
}
遍历期间新增的 c 不会被本次循环看到,因为迭代器绑定的快照里没有它。
弱一致性:ConcurrentHashMap 的迭代器不校验 modCount,按桶逐个推进,不保证反映迭代开始后的修改,也不保证不重复、不遗漏,但绝不会抛 CME。
import java.util.concurrent.ConcurrentHashMap;
public class WeakConsistent {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("a", 1);
for (String key : map.keySet()) {
if (key.equals("a")) map.put("c", 3); // 不抛异常
}
System.out.println(map.size()); // 输出:2
}
}
5. 容易忽略的前提
- 只有结构性修改(增、删、清空)才会让
modCount变化;覆盖已有 key 的 value 不算。fail-fast 是“尽力而为”,多线程下也可能更晚才抛,不能当作正确性保障,它只是尽早暴露 bug 的调试手段。
常见追问
- 为什么 for-each 删除会 CME,而
iterator.remove不会? 后者删除后会同步expectedModCount = modCount。 subList的修改会触发吗? 会,子列表与原列表共享modCount,结构性修改可能抛 CME。- HashMap 的 keySet 迭代器是 fail-fast 吗? 是,
HashIterator同样校验modCount。 - ConcurrentHashMap 的迭代器为什么安全? 弱一致性遍历,不依赖
modCount,只在当前桶内保证一致性。 - fail-safe 一定安全吗? 只是不抛异常,数据可能滞后,需要强一致时仍要加锁或换结构。
小结
fail-fast 用 modCount 把“迭代期间被改”这件事变成可见的异常,优点是零成本、问题暴露早,缺点是并发下不可靠;fail-safe 用快照或弱一致性遍历换取“不中断”,代价是可能读到旧数据。选型时问自己一句:这个场景要的是“绝不读到脏数据”,还是“遍历不能被打断”?前者用 fail-fast 并保证单线程访问,后者用 COW 或 CHM。