Java Fail-Fast与Fail-Safe机制详解
一、前言
在Java日常开发中,我们经常会遇到 ConcurrentModificationException(并发修改异常),绝大多数新手会疑惑:为什么遍历集合时删除元素会报错?多线程操作集合为什么有的报错有的不报错?
这一切的底层根源就是 Java 集合的两大容错机制:
-
Fail-Fast(快速失败):非线程安全集合默认机制,有错立即抛出异常
-
Fail-Safe(安全失败):线程安全并发集合机制,牺牲一致性保证安全
本文将彻底吃透这两个机制,解决开发报错、搞定面试高频问题。
二、基础概念:什么是并发修改?
很多同学有误区:并发修改 = 多线程修改,这是错误的!
Java 集合层面的并发修改定义:
通过迭代器(Iterator/增强for)遍历集合的过程中,对集合进行结构性修改(add/remove/clear)
✅ 包含两种场景:
-
单线程:增强for遍历中调用集合remove/add
-
多线程:一个线程遍历、一个线程修改
三、Fail-Fast 快速失败机制(重点)
3.1 机制定义
Fail-Fast(快速失败) 是 ArrayList、LinkedList、HashMap、HashSet 等非线程安全集合的迭代保护机制。
核心特性:
-
遍历过程中一旦检测到集合结构被修改,立即抛出 ConcurrentModificationException
-
不继续执行、不容忍异常、快速暴露问题
-
目的:避免不可预知的脏数据、遍历错乱问题
3.2 底层核心原理(源码解析)
Fail-Fast 的核心依赖两个全局变量:
-
modCount:集合结构性修改次数(add/remove/clear 都会 ++)
-
expectedModCount:迭代器创建时,记录当前集合的 modCount
3.2.1 核心校验源码(ArrayList)
迭代器每次执行 next() 都会执行校验:
// ArrayList 迭代器核心校验方法
final void checkForComodification() {
// 如果当前修改次数 != 迭代器初始化时的次数,直接抛异常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
3.2.2 完整执行流程
-
创建集合,不断 add,
modCount自增 -
创建迭代器,
expectedModCount = modCount -
遍历途中调用集合
remove/add,modCount++ -
下次遍历执行
checkForComodification,校验失败抛异常
3.3 实战代码示例(触发Fail-Fast)
示例1:单线程增强for遍历删除(最常见报错)
import java.util.ArrayList;
import java.util.List;
/**
* 单线程触发 Fail-Fast 机制
* 报错原因:增强for遍历过程中,使用集合对象的remove方法修改结构
*/
public class FailFastSingleDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Python");
list.add("C++");
// 增强for底层是Iterator迭代器
for (String language : list) {
if ("Python".equals(language)) {
// 集合对象删除:modCount ++,但迭代器expectedModCount不变
list.remove(language);
}
}
}
}
运行结果:直接抛出并发修改异常
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
at java.util.ArrayList$Itr.next(ArrayList.java:861)
示例2:多线程场景触发Fail-Fast
import java.util.ArrayList;
import java.util.List;
/**
* 多线程触发 Fail-Fast
* 线程1遍历,线程2修改集合
*/
public class FailFastMultiDemo {
private static final List<String> list = new ArrayList<>();
public static void main(String[] args) {
list.add("Java");
list.add("Go");
list.add("Rust");
// 遍历线程
new Thread(() -> {
for (String s : list) {
System.out.println("遍历元素:" + s);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 修改线程
new Thread(() -> {
try {
Thread.sleep(200);
list.add("C#");
System.out.println("新增元素成功");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
3.4 避坑:迭代器自带remove为什么不报错?
很多同学疑惑:同样是删除,迭代器的remove就不报错?
原因:迭代器删除后,会同步更新 expectedModCount,保证两端数值一致!
正确安全写法(单线程遍历删除)
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class FailFastCorrectDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Python");
list.add("C++");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
if ("Python".equals(s)) {
// 迭代器内部更新 expectedModCount,校验通过
iterator.remove();
}
}
System.out.println("最终集合:" + list);
}
}
四、Fail-Safe 安全失败机制
4.1 机制定义
Fail-Safe(安全失败) 是 CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentHashMap 等并发集合的机制。
核心特性:
-
遍历不操作原集合,而是操作集合快照副本
-
遍历过程中原集合修改,不会影响副本,永远不抛出并发修改异常
-
牺牲数据实时一致性,换取并发遍历安全
4.2 底层原理
-
迭代器初始化时,拷贝原集合数据生成新副本
-
所有遍历操作基于副本执行
-
原集合的 add/remove 只会修改原数组,不影响遍历副本
-
遍历结束,副本垃圾回收
4.3 优缺点总结
✅ 优点:并发遍历安全,无异常,适合多线程读场景
❌ 缺点:
-
每次修改需要拷贝数组,内存开销大、写性能差
-
只能读取遍历初始化那一刻的数据,无法感知遍历中新增/删除的数据(弱一致性)
-
迭代器不支持 add/remove 操作,会抛出不支持操作异常
4.4 实战代码示例(Fail-Safe无异常)
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Fail-Safe 安全失败机制演示
* 多线程遍历+修改,全程无异常
*/
public class FailSafeDemo {
public static void main(String[] args) {
// 支持Fail-Safe的线程安全集合
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("Java");
list.add("Python");
list.add("C++");
// 遍历线程
new Thread(() -> {
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
System.out.println("遍历元素:" + item);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("✅ 遍历完成,无任何异常");
}).start();
// 修改线程
new Thread(() -> {
try {
Thread.sleep(200);
list.add("Go语言");
System.out.println("✅ 线程2新增元素成功");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
运行结果:全程无异常,遍历只会读取旧数据,新增数据不会被本次遍历读取
遍历元素:Java
遍历元素:Python
✅ 线程2新增元素成功
遍历元素:C++
✅ 遍历完成,无任何异常
五、Fail-Fast vs Fail-Safe 核心对比(面试必背)
| 对比维度 | Fail-Fast 快速失败 | Fail-Safe 安全失败 |
|---|---|---|
| 适用集合 | ArrayList、HashMap、HashSet 等非线程安全集合 | CopyOnWriteArrayList、ConcurrentHashMap 等并发集合 |
| 遍历数据源 | 直接遍历原集合 | 遍历集合快照副本 |
| 并发修改异常 | 会抛出 ConcurrentModificationException | 不会抛出该异常 |
| 数据一致性 | 强一致性,实时感知修改 | 弱一致性,无法感知遍历中新修改的数据 |
| 性能开销 | 无拷贝,开销小,效率高 | 数组拷贝,开销大,写性能低 |
| 设计思想 | 快速暴露问题,避免隐患 | 牺牲一致性,保证并发安全 |
| 适用场景 | 单线程环境 | 多线程、读多写少场景 |
六、开发实战选型规范(生产可用)
-
单线程遍历删除:禁止增强for直接删集合,使用 迭代器自带remove()
-
多线程读多写少:优先使用
CopyOnWriteArrayList(Fail-Safe) -
多线程写多读少:不使用CopyOnWrite(拷贝开销太大),使用
synchronized加锁或ConcurrentHashMap -
追求数据强一致:使用Fail-Fast集合,自行控制并发锁
-
容忍短暂数据不一致:使用Fail-Safe并发集合,提升并发性能
七、面试高频问答(压轴)
Q1:Fail-Fast 机制的原理是什么?
集合维护一个 modCount 修改计数器,迭代器创建时记录 expectedModCount,遍历过程中校验两者是否一致,不一致则抛出并发修改异常,快速失败。
Q2:为什么迭代器remove不报错,集合remove就报错?
集合remove只会让 modCount++,不会修改迭代器的 expectedModCount;迭代器remove会同步更新expectedModCount,保证校验一致。
Q3:Fail-Safe 为什么不会抛并发修改异常?有什么缺点?
遍历基于集合快照副本,原集合修改不影响遍历。缺点是弱一致性、写操作内存开销大、迭代器不支持修改操作。
Q4:ConcurrentHashMap 是 Fail-Fast 还是 Fail-Safe?
JDK1.8+ ConcurrentHashMap 迭代器采用 弱一致性的Fail-Safe设计,不抛并发修改异常,无法感知遍历过程中的部分更新。
八、全文总结
-
Fail-Fast:快速失败,强一致性,有错即抛异常,适用于单线程普通集合
-
Fail-Safe:安全失败,弱一致性,快照遍历无异常,适用于多线程读多写少场景
-
开发中90%的遍历删除报错,都是误用了集合remove,而非迭代器remove
-
并发场景严禁乱用CopyOnWrite集合,写多场景性能会严重退化
更多推荐
所有评论(0)