Java中的Fail-Fast(快速失败)机制和 Fail-Safe(安全失败)机制?
好的,我们来详细讲解 Java 中 Fail-Fast(快速失败)和 Fail-Safe(安全失败)这两种重要的迭代器机制。
核心思想对比
| 特性 | Fail-Fast (快速失败) | Fail-Safe (安全失败) |
|---|---|---|
| 核心思想 | 立即失败。在迭代过程中,一旦发现集合的结构被修改(非通过迭代器自身的方法),立即抛出 ConcurrentModificationException 异常。 |
容忍并发修改。在迭代时,不是直接遍历原始集合,而是基于原始集合的克隆或视图进行遍历。因此不会因并发修改而抛出异常。 |
| 工作原理 | 通过维护一个 modCount(修改次数)变量。每次结构性修改都会增加它的值。迭代器初始化时会记录这个期望值(expectedModCount),每次操作时检查两者是否相等。 |
使用集合的副本、快照或特殊的并发数据结构进行遍历。不关心原始集合是否被修改。 |
| 性能影响 | 无额外开销(除了维护 modCount)。性能好。 | 有显著性能开销:需要复制整个集合或维护额外的数据结构,消耗更多内存和时间。 |
| 数据一致性 | 弱一致性/不一致。抛出异常后,迭代结果不可预测。它不保证能反映出迭代开始时的所有元素状态。 | 弱一致性。反映创建迭代器那一刻的状态,或在遍历过程中反映某些已完成的更新,但不反映正在进行的更新。 |
| 适用场景 | 单线程环境或对数据实时性要求高、不允许脏读的场景。不适用于并发环境。 | 多线程环境或允许在遍历时容忍一定程度的数据不一致的只读场景。适用于并发环境。 |
1. Fail-Fast (快速失败)
a. 工作机制
Fail-Fast 机制主要依赖于 modCount 变量。
modCount:定义在AbstractList等集合中,是一个 int 类型的变量,用于记录集合被结构性修改的次数(如添加、删除元素)。expectedModCount:每个 Iterator/ListIterator 实例内部都有一个此字段,它在初始化时被赋值为当时的modCount。
在调用迭代器的 next(), remove(),previous(),set(),add()等方法时,都会先检查:
java
// Iterator.next() 的简化逻辑
public E next() {
checkForComodification(); // <-- 关键检查点
... // ...其他逻辑
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
如果检测到两者不相等,就说明有其他线程或代码在未经当前迭代器同意的情况下修改了集合结构,于是立刻抛出 ConcurrentModificationException。
b. Demo示例
最常见的例子就是在使用增强 for-loop(底层是 Iterator)时直接删除元素。
java
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class FailFastExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
System.out.println("使用增强for循环:");
try {
for (String s : list) { // foreach底层使用Iterator
if ("B".equals(s)) {
list.remove(s); // ❌错误!直接在外部移除会触发fail-fast
}
}
} catch (ConcurrentModificationException e) {
System.out.println("捕获到 ConcurrentModificationException: " + e);
}
System.out.println("\n使用Iterator自身的remove方法:");
Iterator<String> iterator = list.iterator(); // ✅正确做法:使用iterator.remove()
while (iterator.hasNext()) {
String s = iterator.next();
if ("B".equals(s)) {
iterator.remove(); // ✅这是安全的操作!
}
}
System.out.println("最终列表内容: " + list); // [A, C]
}
}
c. Fail-Fast的实现类与注意事项
- 实现类:几乎所有的非线程安全 Collection(
ArrayList,LinkedList,HashSet,HashMap等)及其对应的 Iterator/ListIterator。 - 注意:Fail-Fast是一种“尽力而为”的机制,不能保证100%检测到所有并发修改,特别是在多线程环境下没有同步措施时。
2. Fail-Safe (安全失败)
a. 工作机制
Fail-Safe机制的核心是:我不在意你改没改原数据。
它通过以下方式实现:
- 对原集合进行深度拷贝或使用快照来创建新的数据结构。
- 或者利用特殊的、为并发设计的内部结构。
由于遍历的是副本而不是原始数据,所以即使原始数据被其他线程疯狂增删改查,也不会影响到当前的遍历过程。ConcurrentHashMap、CopyOnWriteArrayList等就是这样工作的。
b. Demo示例 - CopyOnWriteArrayList
java
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.Iterator;
public class FailSafeExample {
public static void main(String[] args) throws InterruptedException {
CopyOnWriteArrayList<String> safeList = new CopyOnWriteArrayList<>(); // ✅这是一个fail-safe的集合
safeList.add("A");
safeList.add("B");
System.out.println("开始遍历...");
Iterator<String> iterator = safeList.iterator();
Thread t = new Thread(() -> {
try { Thread.sleep(1000); } catch (InterruptedException e) {}
safeList.add("C"); // 🚀另一个线程在遍历期间修改了列表!对于fail-fast这会抛异常。
System.out.println(Thread.currentThread().getName() + "添加了新元素 'C'");
});
t.start();
while(iterator.hasNext()) {
String element = iterator.next();
System.out.println(element);
Thread.sleep(2000); // 🐢模拟处理耗时操作
}
System.out.println("\n最终列表内容: " + safeList);
/*输出可能为:
开始遍历...
A <-第一次打印A后一秒,“C”被加入到了safeList中。
B <-但此时我们拿到的还是旧的快照[‘A’, ‘B’],所以看不到‘C’。这就是弱一致性。
最终列表内容: [A, B, C] */
}
}
从例子中可以看到:
- 没有抛出异常。
- “C”没有被打印出来?因为我们在启动第二个线程之前获取的迭代器是基于
[“A”, “B”]这个快照创建的。CopyOnWriteArrayList的写操作代价很高(要复制数组),但它的读操作非常快且无锁。
c. Fail-Safe的实现类与注意事项
实现类:
java.util.concurrent.ConcurrentHashMapjava.util.concurrent.CopyOnWriteArrayListjava.util.concurrent.CopyOnWriteArraySet- 以及所有实现了
*Iterable*接口的类所返回的*Spliterator*(例如*Arrays.asList(...).spliterator()*)在某些模式下也可能表现出类似行为)
缺点:
- 内存占用大:需要维护一份数据的副本或多个版本。
- 数据可能不是最新的:反映的是某个时间点的状态(“弱一致性”),无法看到在创建副本之后发生的更改。
- 写操作的性能开销大:
CopyOnWriteArrayList每次写入都需要复制整个底层数组。
###总结与选择建议
| 方面 | Fail-Fast | Fail-Safe |
|---|---|---|
| 哲学 | “发现问题立即解决”(抛异常让你知道错了) | “遇到问题绕开走”(给你一份旧数据继续运行) |
| 选择依据 | -单线程程序 -追求高性能读取 -希望及时发现编程错误 |
-多线程程序 -可以接受读取到过时数据 -更看重程序的健壮性而非绝对实时性 |
简单来说:
- 如果你在做普通的单线程开发,几乎总是会遇到并使用到Fail-Fast机制,记住要用Iterator自己的方法来安全地删除元素即可。
- 如果你在进行多线程编程,并且需要在多个线程间共享和修改同一个Collection,那么就应该考虑使用像
ConcurrentHashMap、CopyOnWriteArrayList这样的Fail-Safe容器来保证程序的稳定性。