⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 小哈学Java 「犬小哈」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

先给结论

fail-fast 和 fail-safe 是 Java 集合迭代器面对”遍历途中集合被改了”这件事的两种态度:

  • fail-fast:立刻翻脸,抛 ConcurrentModificationException。代表:ArrayList、HashMap、HashSet
  • fail-safe:假装没看见,在副本或弱一致性视图上继续遍历。代表:CopyOnWriteArrayList、ConcurrentHashMap

一句话:fail-fast 是”发现问题立刻报错”,fail-safe 是”容忍修改,保证过程不中断”。

modCount:fail-fast 的核心把戏

实现极其简单粗暴。ArrayList、HashMap 内部维护了一个 modCount 计数器,每次结构性修改(add / remove)都会 +1。

迭代器创建时,把当前 modCount 快照为 expectedModCount。之后每次调 next() 都做一次核验:

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

数字对不上?直接炸。

注意:这个检测不是线程安全的——modCount 没加 volatile,多线程下不保证能及时发现。fail-fast 的定位是”尽力而为的 bug 检测”,不是并发控制机制。

fail-safe 的两种实现套路

CopyOnWriteArrayList:快照隔离

创建迭代器时,直接持有底层数组的引用。之后原集合的写操作会把整个数组复制一份再修改,迭代器看到的还是旧数组,和写操作完全隔离。

代价很明确:每次写都拷贝整个数组。适合读极多、写极少、数据量不大的场景。大数据量 + 频繁写入 = 内存爆炸。

ConcurrentHashMap:弱一致性

不拷贝数据,迭代器直接在当前数据结构上穿行。不抛异常,但不保证能看到迭代开始后的所有修改——可能看到,也可能看不到。

这是性能和一致性之间的刻意折中。绝大多数并发场景下,弱一致性够用了。

代码说话

import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

publicclass FailFastVsFailSafeDemo {
public static void main(String[] args) {
System.out.println("=== Fail-Fast (ArrayList) ===");
List<String> fastList = new ArrayList<>(Arrays.asList("A", "B", "C"));
try {
for (String s : fastList) {
System.out.println(s);
if ("B".equals(s)) {
fastList.remove("B"); // 迭代中改原集合 → 炸
}
}
} catch (ConcurrentModificationException e) {
System.out.println("捕获: " + e.getClass().getSimpleName());
}

System.out.println("\n=== Fail-Safe (CopyOnWriteArrayList) ===");
List<String> safeList = new CopyOnWriteArrayList<>(Arrays.asList("A", "B", "C"));
for (String s : safeList) {
System.out.println(s);
if ("B".equals(s)) {
safeList.remove("B"); // 改的是新数组,迭代不受影响
}
}
System.out.println("迭代后: " + safeList); // [A, C]
}
}

输出:

=== Fail-Fast (ArrayList) ===
A
B
捕获: ConcurrentModificationException

=== Fail-Safe (CopyOnWriteArrayList) ===
A
B
C
迭代后: [A, C]

对比速查表

维度 Fail-Fast Fail-Safe / Weakly Consistent
核心态度 发现被改,立刻炸 允许修改,保证不炸
抛异常 ConcurrentModificationException 不抛
底层数据 直接操作原集合 副本或弱一致性视图
性能开销 每次迭代仅做整数比较,几乎零开销 可能涉及数组拷贝,内存和 CPU 开销较大
适用场景 单线程,或确认迭代中不会改集合 高并发读多写少

实战踩坑点

  1. for-each 里直接 remove 是经典 bug。for-each 底层就是迭代器,直接调 list.remove() 会触发 fail-fast。正确做法:用 Iterator.remove(),或 JDK 8+ 的 Collection.removeIf()。

  2. fail-safe ≠ 线程安全。ConcurrentHashMap 的迭代器不抛异常,但复合操作(check-then-act)仍需额外同步。不要以为不炸就安全了。

  3. 选型口诀:单线程 → ArrayList / HashMap;并发读多写少、数据量小 → CopyOnWriteArrayList;通用并发场景 → ConcurrentHashMap。

文章目录
  1. 1. 先给结论
  2. 2. modCount:fail-fast 的核心把戏
  3. 3. fail-safe 的两种实现套路
    1. 3.1. CopyOnWriteArrayList:快照隔离
    2. 3.2. ConcurrentHashMap:弱一致性
  4. 4. 代码说话
  5. 5. 对比速查表
  6. 6. 实战踩坑点