fail-fast 和 fail-safe 的区别?

2025年 阅读约 7 分钟 面试指南 · Java面试

深入解析Java集合的fail-fast和fail-safe机制,从ConcurrentModificationException到CopyOnWriteArrayList,分初级、中级、高级三个层次全面讲解。

一句话总结

fail-fast 通过 modCount 检测并发修改(迭代器创建时记录 expectedModCount,每次操作比对,不等则抛 ConcurrentModificationException)。fail-safe 操作副本(CopyOnWriteArrayList 写时复制,ConcurrentHashMap 弱一致性迭代器),不抛异常但可能读到旧数据。fail-fast 只是尽力检测,不能依赖它保证正确性。

初级理解

fail-fast(快速失败):迭代过程中如果集合被结构性修改(add/remove),迭代器立即抛出 ConcurrentModificationException。

fail-safe(安全失败):迭代过程中即使集合被修改,也不会抛异常,因为迭代的是集合的快照(副本)。

对比fail-fastfail-safe
代表集合ArrayList, HashMap, HashSetCopyOnWriteArrayList, ConcurrentHashMap
检测机制modCount 计数器不检测,操作副本
异常ConcurrentModificationException不抛异常
内存开销无额外开销需要复制集合(内存大)
数据一致性强一致弱一致(可能读到旧数据)
// fail-fast 示例 List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C")); for (String s : list) { list.remove(s); // ConcurrentModificationException } // fail-safe 示例 List<String> list2 = new CopyOnWriteArrayList<>(Arrays.asList("A", "B", "C")); for (String s : list2) { list2.remove(s); // 不抛异常,正常执行 }

中级深入

fail-fast 的实现原理(modCount):集合内部维护一个 modCount 字段,每次结构性修改(add/remove)时 modCount++。迭代器创建时记录 expectedModCount = modCount,每次 next()/remove() 时检查 expectedModCount == modCount,不等则抛异常。

// ArrayList.Itr 的 checkForComodification final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }

fail-fast 不能保证一定检测到:modCount 可能溢出回绕,且只在迭代器方法被调用时才检查。所以 fail-fast 只是"尽力而为"的检测机制,不能依赖它来保证正确性。

高级拓展

CopyOnWriteArrayList 的原理:写操作(add/remove/set)时加 ReentrantLock,复制整个数组,在新数组上修改,最后替换引用。读操作不加锁,直接读当前数组。

适用场景:读多写少的场景(如监听器列表、配置信息)。写操作频繁时不适用(每次写都要复制整个数组,开销巨大)。

// CopyOnWriteArrayList 的 add 方法(简化) public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } }

ConcurrentHashMap 的弱一致性:迭代器不抛 ConcurrentModificationException,遍历的是创建迭代器时的"快照"(但不复制整个 Map),允许遍历过程中其他线程修改。

面试加分项:能说出 CopyOnWriteArrayList 的写时复制原理和适用场景。

实战场景

场景:遍历时删除的正确姿势汇总

// 方式1:Iterator.remove()(所有集合通用) Iterator<String> it = list.iterator(); while (it.hasNext()) { if ("target".equals(it.next())) it.remove(); } // 方式2:removeIf(JDK 8+,最简洁) list.removeIf(s -> "target".equals(s)); // 方式3:倒序遍历(仅 ArrayList 适用) for (int i = list.size() - 1; i >= 0; i--) { if ("target".equals(list.get(i))) list.remove(i); } // 方式4:收集后批量删除 List<String> toRemove = list.stream() .filter(s -> "target".equals(s)) .collect(Collectors.toList()); list.removeAll(toRemove); // 方式5:CopyOnWriteArrayList(并发场景) CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>(); for (String s : cowList) { cowList.remove(s); // 不抛异常 }

面试模拟

Q:单线程中也会触发 fail-fast 吗?

A:会。fail-fast 不区分单线程还是多线程,只要迭代过程中结构性修改了集合就会触发。最常见的场景是 for-each 循环中直接调用 list.remove(),这是单线程操作但同样抛异常。

Q:CopyOnWriteArrayList 适合什么场景?为什么不适合写多?

A:适合读多写少(如监听器列表、配置缓存)。因为每次写操作都要 Arrays.copyOf 复制整个数组,写多时内存和 CPU 开销巨大。另外它保证最终一致性而非强一致性——写操作完成后,其他线程的读可能短暂看到旧数据。