图片

技术解析

什么是Fail-Fast?

Fail-Fast是Java集合(如 ArrayListHashMap)中迭代器(Iterator)的一种行为模式。

定义:当一个迭代器被创建后,如果它所遍历的集合在结构上被修改了(比如添加或删除了元素),并且这种修改不是通过迭代器自身的remove()方法进行的,那么迭代器在下一次操作时,会立刻抛出 ConcurrentModificationException 异常。

重要澄清:Fail-Fast是一种错误检测机制,而不是一种并发控制或线程安全机制。它只能“尽力而为”地发现问题,并不能保证一定能发现所有的并发修改。

它是如何工作的?modCount 的魔力

  1. 1. modCount (修改计数器):
    在像ArrayList这样的集合内部,有一个私有成员变量 modCount任何对集合进行结构性修改的操作(如add()remove()clear())都会使这个计数器加一

  2. 2. expectedModCount (期望修改计数):
    当你调用list.iterator()创建一个迭代器时,迭代器会记录下当时集合的modCount,并保存在自己的一个内部变量expectedModCount中。

  3. 3. 一致性检查:
    在迭代器的每一次后续操作中(如调用hasNext()next()),它都会先比较自己保存的expectedModCount是否还等于集合当前的modCount

  4. 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. 1. 盖上“魔法封条” (modCount & expectedModCount):
    在《户籍总册》的封面上,有一个“最后修订版本号”,比如是“版本 V10”(这就是modCount)。
    当你作为普查员,开始工作前,你会在自己的工作笔记上郑重地写下:“本次普查基于‘版本 V10’进行。”(这就是expectedModCount)。

  2. 2. 严谨的工作流程:
    你开始从第一页逐行普查。每普查完一行,准备普查下一行之前,你都会执行一个雷打不动的动作:抬头看一眼《户籍总册》封面上的版本号,再低头看一眼自己笔记上的版本号,确认两者是否一致。

  3. 3. “撕毁封条”的时刻 (并发修改):
    当你正在普查第50页时,一位户籍管理员匆匆跑进来,在名册的第2页上添加了一个新生儿的名字(list.add())。按照规定,他必须立刻将封面上的版本号更新为“版本 V11” (modCount++)。

  4. 4. “快速失败”的反应:
    你普查完第50页,准备翻到第51页。你习惯性地抬头一看,发现名册的版本号变成了“V11”!你再低头看自己的笔记,上面写的是“V10”。
    你大惊失色,立刻合上所有文件,大喊一声:“停止工作!有人在我普查期间修改了名册!我的数据可能已经不准了,这份普查报告我不能再继续下去!”(抛出ConcurrentModificationException)。

  • • 为什么这么做?
    你的“快速失败”,并不是为了阻止户籍管理员修改名册。而是为了保护你最终报告的严谨性。因为名册被修改后,你可能会漏掉那个新生儿,或者因为页码错乱而重复统计某个人。与其提交一份可能错误的数据,不如立刻、大声地把问题暴露出来,让城市的管理者(程序员)去修复“一边普查,一边改名册”这个流程上的Bug。

Fail-Safe — “复印件工作模式”

  • • 工作方式:
    Fail-Safe(安全失败)的普查员则采用另一种方式。在开始工作前,他会把整本《户籍总册》完整地复印一份CopyOnWriteArrayList在创建迭代器时会复制底层数组)。

  • • 效果:
    现在,他完全在自己的“复印件”上进行普查。无论户籍管理员在原版上如何增添删改,都与他手上的复-印件无关。他绝对不会遇到版本不一致的问题,可以安安稳稳地完成普查。

  • • 代价:
    他的普查报告,反映的只是复印那一刻的城市人口状况,所有最新的新生儿信息,都不会包含在他的报告里。他牺牲了数据的实时性,换取了工作的绝对稳定性

故事总结:

迭代器类型

Fail-Fast (带封条的普查员) Fail-Safe (用复印件的普查员)
工作原理 版本号检查

 (modCount)

操作数据快照/副本
修改后行为 立刻抛出异常

 (ConcurrentModificationException)

无视修改,继续遍历旧数据
数据一致性

无法保证,但能发现问题

不一致

 (遍历的是旧数据)

核心比喻 “数据被动了,我不干了!” “我只管我的复印件,外面与我无关。”
典型集合 ArrayList

HashMap

CopyOnWriteArrayList

ConcurrentHashMap

结论:
Fail-Fast是一种重要的防御性编程思想。它通过一种简单高效的方式,帮助我们在开发阶段尽早发现并发修改的逻辑错误,避免在生产环境中出现更难以追踪的、不确定的行为。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐