Fail-Fast vs Fail-Safe:一个“当场翻脸”,一个“假装没看见”
Fail-Fast是Java集合(如ArrayListHashMap)中迭代器(Iterator)的一种行为模式。定义:当一个迭代器被创建后,如果它所遍历的集合在结构上被修改了(比如添加或删除了元素),并且这种修改不是通过迭代器自身的remove()方法进行的,那么迭代器在下一次操作时,会立刻抛出异常。重要澄清:Fail-Fast是一种错误检测机制,而不是一种并发控制或线程安全机制。它只能“尽力而

技术解析
什么是Fail-Fast?
Fail-Fast是Java集合(如 ArrayList, HashMap)中迭代器(Iterator)的一种行为模式。
定义:当一个迭代器被创建后,如果它所遍历的集合在结构上被修改了(比如添加或删除了元素),并且这种修改不是通过迭代器自身的
remove()方法进行的,那么迭代器在下一次操作时,会立刻抛出ConcurrentModificationException异常。
重要澄清:Fail-Fast是一种错误检测机制,而不是一种并发控制或线程安全机制。它只能“尽力而为”地发现问题,并不能保证一定能发现所有的并发修改。
它是如何工作的?modCount 的魔力
-
1.
modCount(修改计数器):
在像ArrayList这样的集合内部,有一个私有成员变量modCount。任何对集合进行结构性修改的操作(如add(),remove(),clear())都会使这个计数器加一。 -
2.
expectedModCount(期望修改计数):
当你调用list.iterator()创建一个迭代器时,迭代器会记录下当时集合的modCount值,并保存在自己的一个内部变量expectedModCount中。 -
3. 一致性检查:
在迭代器的每一次后续操作中(如调用hasNext(),next()),它都会先比较自己保存的expectedModCount是否还等于集合当前的modCount。 -
4. 抛出异常:
如果不相等,迭代器就知道:“在我上次检查之后,有人动了我的集合!” 于是,它会立刻抛出ConcurrentModificationException。
代码示例:触发 ConcurrentModificationException
这个例子甚至在单线程中也能触发Fail-Fast,这证明了它与线程安全无关,而与“意外的”集合修改有关。
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class FailFastDemo {
public static void main(String[] args) {
System.out.println("--- 1. 演示 Fail-Fast ---");
List<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Cherry");
try {
// 使用 for-each 循环(其底层就是迭代器)
for (String fruit : list) {
System.out.println("Processing: " + fruit);
if ("Banana".equals(fruit)) {
// 错误操作:在遍历时,直接通过集合本身去修改,而不是通过迭代器
list.remove("Banana"); // 这会导致 modCount 增加
}
}
} catch (ConcurrentModificationException e) {
System.err.println("\n错误!触发了 ConcurrentModificationException!");
System.err.println("迭代器期望的 modCount 和集合当前的 modCount 不一致了。");
}
System.out.println("\n--- 2. 演示 Fail-Safe ---");
// CopyOnWriteArrayList 是一个线程安全的List,它的迭代器是“安全失败”的
List<String> safeList = new CopyOnWriteArrayList<>();
safeList.add("Apple");
safeList.add("Banana");
safeList.add("Cherry");
Iterator<String> iterator = safeList.iterator(); // 迭代器在创建时,获取了集合的一个快照
System.out.println("开始遍历 Fail-Safe 列表...");
while (iterator.hasNext()) {
String fruit = iterator.next();
System.out.println("Processing: " + fruit);
if ("Banana".equals(fruit)) {
// 在遍历时修改集合
safeList.remove("Banana");
System.out.println(" (在遍历'Banana'时,已从原始列表中移除了'Banana')");
}
}
System.out.println("\n遍历完成,没有抛出异常。");
System.out.println("原始列表的最终内容: " + safeList);
}
}
故事场景:严谨的人口普查
-
• 《城市户籍总册》 (集合
Collection): 一本记录了全城所有居民信息的官方名册。 -
• 你 (迭代器
Iterator): 一位被委派的、极其严谨的人口普查员。 -
• 户籍管理员 (其他线程或代码): 负责在《户籍总册》上添加新生儿或注销逝者。
Fail-Fast — “带魔法封条的工作模式”
-
1. 盖上“魔法封条” (
modCount&expectedModCount):
在《户籍总册》的封面上,有一个“最后修订版本号”,比如是“版本 V10”(这就是modCount)。
当你作为普查员,开始工作前,你会在自己的工作笔记上郑重地写下:“本次普查基于‘版本 V10’进行。”(这就是expectedModCount)。 -
2. 严谨的工作流程:
你开始从第一页逐行普查。每普查完一行,准备普查下一行之前,你都会执行一个雷打不动的动作:抬头看一眼《户籍总册》封面上的版本号,再低头看一眼自己笔记上的版本号,确认两者是否一致。 -
3. “撕毁封条”的时刻 (并发修改):
当你正在普查第50页时,一位户籍管理员匆匆跑进来,在名册的第2页上添加了一个新生儿的名字(list.add())。按照规定,他必须立刻将封面上的版本号更新为“版本 V11” (modCount++)。 -
4. “快速失败”的反应:
你普查完第50页,准备翻到第51页。你习惯性地抬头一看,发现名册的版本号变成了“V11”!你再低头看自己的笔记,上面写的是“V10”。
你大惊失色,立刻合上所有文件,大喊一声:“停止工作!有人在我普查期间修改了名册!我的数据可能已经不准了,这份普查报告我不能再继续下去!”(抛出ConcurrentModificationException)。
-
• 为什么这么做?
你的“快速失败”,并不是为了阻止户籍管理员修改名册。而是为了保护你最终报告的严谨性。因为名册被修改后,你可能会漏掉那个新生儿,或者因为页码错乱而重复统计某个人。与其提交一份可能错误的数据,不如立刻、大声地把问题暴露出来,让城市的管理者(程序员)去修复“一边普查,一边改名册”这个流程上的Bug。
Fail-Safe — “复印件工作模式”
-
• 工作方式:
Fail-Safe(安全失败)的普查员则采用另一种方式。在开始工作前,他会把整本《户籍总册》完整地复印一份(CopyOnWriteArrayList在创建迭代器时会复制底层数组)。 -
• 效果:
现在,他完全在自己的“复印件”上进行普查。无论户籍管理员在原版上如何增添删改,都与他手上的复-印件无关。他绝对不会遇到版本不一致的问题,可以安安稳稳地完成普查。 -
• 代价:
他的普查报告,反映的只是复印那一刻的城市人口状况,所有最新的新生儿信息,都不会包含在他的报告里。他牺牲了数据的实时性,换取了工作的绝对稳定性。
故事总结:
|
迭代器类型 |
Fail-Fast (带封条的普查员) | Fail-Safe (用复印件的普查员) |
| 工作原理 | 版本号检查
( |
操作数据快照/副本 |
| 修改后行为 | 立刻抛出异常
( |
无视修改,继续遍历旧数据 |
| 数据一致性 |
无法保证,但能发现问题 |
不一致
(遍历的是旧数据) |
| 核心比喻 | “数据被动了,我不干了!” | “我只管我的复印件,外面与我无关。” |
| 典型集合 | ArrayList
, |
CopyOnWriteArrayList
, |
结论:
Fail-Fast是一种重要的防御性编程思想。它通过一种简单高效的方式,帮助我们在开发阶段尽早发现并发修改的逻辑错误,避免在生产环境中出现更难以追踪的、不确定的行为。
更多推荐
所有评论(0)