什么是 fail-fast?什么是 fail-safe?

#Java#面试八股

什么是 fail-fast?什么是 fail-safe?

结论先行:fail-fast 是“发现并发修改就立刻抛异常”的检测机制:集合内部维护 modCount,迭代器创建时记下 expectedModCount,每次取元素前比对,不一致就抛 ConcurrentModificationException。fail-safe 不是 JDK 的正式术语,通常指弱一致性迭代器或写时复制容器:迭代基于快照或并发结构,不抛 CME,但可能看不到最新修改。

1. 两种策略对比

维度fail-fastfail-safe
典型容器ArrayList、HashMap、HashSetCopyOnWriteArrayList、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。

笔记加载中…