Skip to content

Fail-Fast:集合的快速失败机制

为什么需要 Fail-Fast

想象一个场景:你在遍历一个 List,同时另一个线程在修改它。

线程 A:遍历 list  [a] [b] [c]
线程 B:修改 list  add(d)

        [a] [b] [c] [d]

线程 A 继续遍历...但结构已经变了,可能读到重复或漏掉元素

Fail-Fast 就是为了尽早发现这种问题并报错,而不是默默产生错误结果。


Fail-Fast 的原理

每个集合内部维护一个 modCount(修改计数器):

java
// ArrayList 的 modCount 字段
protected transient int modCount = 0;

// 每次 add/remove/clear 操作,modCount++
public boolean add(E e) {
    ensureCapacityInternal(size + 1);
    elementData[size++] = e;
    modCount++;  // ← 结构性修改
    return true;
}

迭代器在创建时会记录当时的 modCount

java
// 迭代器内部
private class Itr implements Iterator<E> {
    int expectedModCount = modCount; // ← 创建时记录

    public E next() {
        checkForComodification(); // ← 每次 next 前检查
        return elementData[cursor++];
    }

    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

如果遍历过程中 modCount 变了,说明集合被修改了,迭代器立刻抛异常。


单线程中的 Fail-Fast

java
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));

// ❌ 增强 for 循环中删除
for (String s : list) {
    if ("b".equals(s)) {
        list.remove(s); // ConcurrentModificationException!
    }
}

// ❌ 普通 for 循环中删除(可能漏掉元素)
for (int i = 0; i < list.size(); i++) {
    if ("b".equals(list.get(i))) {
        list.remove(i); // 删除后,后面的元素前移,i 没有回退
        // 下一个元素被跳过了!
    }
}

// ✅ 正确方式:Iterator.remove()
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if ("b".equals(it.next())) {
        it.remove(); // ✅ 安全,expectedModCount 会同步更新
    }
}

多线程中的 Fail-Fast

java
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));

// 线程 1:遍历
Thread t1 = new Thread(() -> {
    for (int i : list) { // ❌ Fail-Fast
        System.out.println(i);
        try { Thread.sleep(50); } catch (InterruptedException ignored) {}
    }
});

// 线程 2:修改
Thread t2 = new Thread(() -> {
    try { Thread.sleep(25); } catch (InterruptedException ignored) {}
    list.remove(0); // ❌ 修改了集合
});

t1.start();
t2.start();
t1.join();
t2.join();
// 可能抛出 ConcurrentModificationException

Fail-Safe:另一种选择

Fail-Safe 集合在遍历时操作的是副本,不会抛出异常:

集合遍历方式特点
ArrayList / HashSetFail-Fast遍历原集合,快速检测修改
CopyOnWriteArrayListFail-Safe遍历副本,写时复制
ConcurrentHashMapFail-Safe遍历时允许并发修改
synchronizedListFail-Fast加了锁但仍是原集合

CopyOnWriteArrayList

java
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(
    Arrays.asList("a", "b", "c")
);

for (String s : list) {
    System.out.println(s);
    if ("b".equals(s)) {
        list.add("d"); // ✅ 不抛异常,遍历的是创建时的快照
    }
}

ConcurrentHashMap

java
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("a", 1);
map.put("b", 2);

for (Map.Entry<String, Integer> e : map.entrySet()) {
    System.out.println(e.getKey());
    map.put("c", 3); // ✅ 不抛异常,部分新增可能看不到
}

Fail-Fast 不保证检测到所有修改

Fail-Fast 是尽力而为,不是绝对的。如果修改没有影响 modCount,就不会被检测到:

java
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));

// ❌ set() 不改变 modCount,所以 Fail-Fast 不会检测到
Iterator<String> it = list.iterator();
list.set(1, "B"); // 修改了索引 1,但 modCount 没变
// 遍历时仍然能继续,不会抛异常
while (it.hasNext()) {
    System.out.println(it.next()); // 但遍历结果可能不符合预期
}

常见误区

误区 1:Fail-Fast 只在多线程中发生

实际上,单线程中也会触发。比如在增强 for 循环中调用 list.remove()

误区 2:Fail-Fast 一定能检测到并发修改

不保证。只能检测到「结构性修改」(add/remove/clear),set() 不会触发。

误区 3:try-catch ConcurrentModificationException 就能继续

不应该捕获后继续。抛异常说明代码有问题,应该修复遍历时的删除逻辑。


正确处理 Fail-Fast

场景正确做法
遍历时删除Iterator.remove()
多线程共享集合用并发集合(CopyOnWriteArrayList 等)
需要遍历时修改ListIterator.set() 或 CopyOnWriteArrayList
想避免 Fail-FastIteratorforEachRemaining 或 Stream

总结

要点说明
触发条件遍历时结构性修改(add/remove/clear)
触发机制modCount 计数器不一致
检测范围只检测结构性修改,set() 不检测
Fail-Safe遍历副本,ConcurrentHashMap、CopyOnWriteArrayList
安全删除iterator.remove()

一句话:Fail-Fast 是集合的「安全带」——出问题时宁可抛异常,也不让你默默出错。


相关链接

基于 VitePress 构建