Fail-Safe 与 Fail-Fast 机制分别有什么作用
Fail-Safe 与 Fail-Fast 机制分别有什么作用
面试指北 · 蔚来
题意
这道题表面是在问两个英文名词,实际考察的是:Java 集合在遍历过程中如果被修改,不同容器会采用什么处理策略,以及这些策略背后的原理、代价和适用场景。
也就是说,面试官通常希望你讲清楚:
- 什么是 Fail-Fast,什么情况下会触发;
- 什么是 Fail-Safe,它为什么不会抛
ConcurrentModificationException; - 两类机制分别对应哪些常见容器;
- 它们和“线程安全”“强一致性”是不是一回事;
- 实际开发中应该怎么选,容易踩哪些坑。
结论
Fail-Fast 和 Fail-Safe 是集合遍历时对“遍历期间集合被修改”的两种不同处理机制。
- Fail-Fast:快速失败。 在遍历过程中一旦发现集合发生了结构性修改,就立刻抛出
ConcurrentModificationException,让问题尽早暴露。典型容器有HashMap、ArrayList等java.util下的常见集合。 - Fail-Safe:失败安全。 遍历过程中即使原集合被修改,也不会抛出
ConcurrentModificationException。它通常通过快照或弱一致性遍历实现,因此遍历时可能看不到最新修改。典型容器有CopyOnWriteArrayList、ConcurrentHashMap等并发容器。 - 关键区别: Fail-Fast 更强调“发现并发修改就报错”,Fail-Safe 更强调“允许并发修改继续遍历”。但要注意,Fail-Fast 不等于线程安全,Fail-Safe 也不等于强一致。
要点展开
1. Fail-Fast:快速失败,尽早暴露修改
Fail-Fast 的核心表现是:使用迭代器遍历集合时,如果集合在遍历过程中被结构性修改,迭代器会尽快抛出 ConcurrentModificationException,从而导致遍历失败。
例如定义一个 Map,用 Iterator 遍历,在遍历过程中向集合里新增元素:
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
Iterator<String> iterator = map.keySet().iterator();
while (iterator.hasNext()) {
String key = iterator.next();
if ("a".equals(key)) {
map.put("b", 2); // 结构性修改,下一次迭代器检查时可能抛出 CME
}
}
这种情况就属于 Fail-Fast。原文中的示意图如下:

它背后的常见原理是:集合内部维护一个类似 modCount 的修改次数,迭代器创建时记录一个期望值 expectedModCount。每次调用 next()、remove() 等操作时,迭代器会检查 modCount 是否还等于 expectedModCount,如果不等,就抛出 ConcurrentModificationException。
这里有几个容易忽略的点:
add、remove、clear、向HashMap中插入新 key 等,通常属于结构性修改;- 如果只是替换已有 key 的 value,一般不算结构性修改,不一定会触发 Fail-Fast;
- 使用迭代器自己的
iterator.remove()删除元素是允许的,因为它会同步更新expectedModCount; - 但如果直接调用集合的
map.remove()、list.remove(),就可能触发异常。
java.util 包下的常见集合类大多采用快速失败机制,例如 HashMap、ArrayList、HashSet、LinkedList 等。它们适合在单线程场景下使用;如果在多线程环境共享修改,应该使用并发容器或额外加锁,而不是依赖 Fail-Fast 来保证安全。
2. Fail-Safe:失败安全,基于快照或弱一致性遍历
Fail-Safe 的核心表现是:遍历过程中修改集合,不会抛出 ConcurrentModificationException,遍历可以继续完成。
但“不抛异常”不代表“能看到最新数据”。Fail-Safe 通常有两种实现思路:
- 快照式遍历:遍历开始时基于原集合的一份拷贝进行,后续对原集合的修改不会影响这次遍历。
CopyOnWriteArrayList就是典型代表。 - 弱一致性遍历:迭代器不保证看到遍历开始后的所有修改,也不保证看不到部分修改,但不会因为并发修改而抛出
ConcurrentModificationException。ConcurrentHashMap的迭代器就是弱一致性的。
以 CopyOnWriteArrayList 为例:
List<String> list = new CopyOnWriteArrayList<>();
list.add("A");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String value = iterator.next();
if ("A".equals(value)) {
list.add("B");
}
System.out.println(value);
}
System.out.println(list);
遍历过程中虽然向集合新增了 "B",但不会抛出异常。由于迭代器遍历的是创建迭代器那一刻的快照,所以本次遍历通常只打印 "A",而集合本身最后会包含 "A" 和 "B"。
原文中的示意图如下:

CopyOnWriteArrayList 的原理是“写时复制”:读操作直接读当前数组,写操作会加锁并复制出一个新数组,修改完成后再把引用指向新数组。这样读线程可以在旧数组快照上安全遍历,不会被写操作打断。代价是每次写都要复制数组,内存开销和写性能成本较高,因此更适合读多写少、数据量不大、允许最终一致的场景。
ConcurrentHashMap 则不是简单地复制整个 Map。它的迭代器是弱一致性的:遍历过程中不会抛出 ConcurrentModificationException,但也不保证反映遍历开始后的所有修改,size() 等结果在并发场景下也更接近估计值。它更适合高并发的键值读写场景。
原文提到“java.util.concurrent 包下的容器都是安全失败的”,可以理解为:这些并发容器在遍历时通常不会像 HashMap、ArrayList 那样抛 ConcurrentModificationException。但实现上要区分清楚:CopyOnWriteArrayList 是写时复制快照,ConcurrentHashMap 是弱一致性遍历,并不是所有并发容器都会完整复制一份集合。
3. 实际使用中怎么选
- 如果是单线程遍历,或者希望尽早发现“边遍历边修改”的代码 bug,使用普通集合的 Fail-Fast 机制即可,让异常帮助定位问题。
- 如果是读多写少、遍历频繁、写操作不频繁的场景,可以考虑
CopyOnWriteArrayList。 - 如果是高并发键值读写场景,优先考虑
ConcurrentHashMap,接受迭代器的弱一致性。 - 如果业务必须要求遍历期间看到一份完全稳定、强一致的快照,那么不能只依赖 Fail-Safe,需要根据场景加锁或在业务层复制集合。
易错点与对比
| 对比维度 | Fail-Fast | Fail-Safe |
|---|---|---|
是否抛 ConcurrentModificationException |
通常会抛 | 通常不抛 |
| 遍历对象 | 原集合 | 快照或弱一致性视图 |
| 是否能看到最新修改 | 单线程下一致,但并发下不可靠 | 可能看不到遍历开始后的修改 |
| 典型容器 | ArrayList、HashMap、HashSet |
CopyOnWriteArrayList、ConcurrentHashMap |
| 主要目的 | 尽早暴露并发修改问题 | 允许并发修改下继续遍历 |
| 是否等于线程安全 | 不等于 | 也不等于强一致 |
几个常见误区:
- Fail-Fast 不是线程安全机制。 它只是“尽力检测”修改,并不能保证一定发现所有并发问题,更不能替代锁或并发容器。
ConcurrentModificationException不只在多线程中出现。 单线程里边遍历边直接修改集合,同样会触发。- 增强 for 循环底层也是迭代器。 所以
for (String s : list) { list.remove(s); }也可能触发 Fail-Fast。 - Fail-Safe 不是强一致性。 它只是不抛异常,可能读到旧数据,也可能看不到新增元素。
java.util.concurrent下的容器不都是“复制整个集合”。CopyOnWriteArrayList是写时复制,ConcurrentHashMap是弱一致性遍历。- Fail-Safe 迭代器不一定支持修改。 例如
CopyOnWriteArrayList的迭代器通常只读,调用remove()可能抛UnsupportedOperationException。 - Fail-Fast 的检测不是绝对可靠的。 在并发环境下,它只能帮助尽早暴露问题,不能作为并发控制手段。
小结
面试时可以按这个顺序回答:
Fail-Fast 是快速失败机制,常见于
ArrayList、HashMap等普通集合。迭代器通过modCount和expectedModCount检测遍历期间的结构性修改,发现不一致就抛ConcurrentModificationException,目的是尽早暴露“边遍历边修改”的问题。
Fail-Safe 是失败安全机制,常见于CopyOnWriteArrayList、ConcurrentHashMap等并发容器。它通过快照或弱一致性遍历避免抛异常,但遍历期间可能看不到最新修改,不保证强一致。
两者不是简单的“线程安全/不安全”之分:Fail-Fast 不等于线程安全,Fail-Safe 也不等于强一致。实际使用时,要根据读写比例、数据一致性和性能要求选择。
一句话总结:Fail-Fast 是“发现修改就尽快报错”,Fail-Safe 是“允许修改继续遍历,但接受弱一致”。