在 Java 集合中,fail-fast 和 fail-safe 是两种不同的遍历机制,用来定义在遍历集合时是否允许修改集合内容。它们的区别在于:
- fail-fast:不允许在遍历过程中修改集合,一旦发现修改,立刻抛出异常。
- fail-safe:允许在遍历过程中修改集合,因为它实际上是操作集合的"副本"。
fail-fast(快速失败)
fail-fast 的特点
- 定义 :在遍历集合时,如果检测到集合结构被修改(比如增删改),会立刻抛出
ConcurrentModificationException
异常。 - 应用场景 :
java.util
包下的集合(例如ArrayList
、HashMap
等)通常使用 fail-fast 机制。 - 发生原因 :通过迭代器(
Iterator
)遍历时,如果在外部修改了集合内容,迭代器会检查集合的结构是否发生变化。一旦发现不一致,就触发异常。
单线程环境下 fail-fast 的例子
示例 1:ArrayList
的 fail-fast
java
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i + "");
}
Iterator<String> iterator = list.iterator();
int i = 0;
while (iterator.hasNext()) {
if (i == 3) {
list.remove(3); // 修改集合,触发 fail-fast
}
System.out.println(iterator.next());
i++;
}
}
运行结果 :程序抛出 ConcurrentModificationException
,因为在迭代过程中直接修改了集合。
示例 2:HashMap
的 fail-fast
java
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
for (int i = 0; i < 10; i++) {
map.put(i + "", i + "");
}
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
int i = 0;
while (it.hasNext()) {
if (i == 3) {
map.remove("3"); // 修改集合,触发 fail-fast
}
Map.Entry<String, String> entry = it.next();
System.out.println("key= " + entry.getKey() + ", value= " + entry.getValue());
i++;
}
}
运行结果 :同样会抛出 ConcurrentModificationException
。
多线程环境下 fail-fast 的例子
如果在多线程中,一个线程在遍历集合,另一个线程修改集合,也会触发 fail-fast。
java
public class FailFastTest {
public static List<String> list = new ArrayList<>();
private static class MyThread1 extends Thread {
@Override
public void run() {
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(this.getName() + ": " + iterator.next());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private static class MyThread2 extends Thread {
int i = 0;
@Override
public void run() {
while (i < 10) {
if (i == 2) {
list.remove(i); // 修改集合
}
System.out.println("thread2: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
i++;
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
list.add(i + "");
}
MyThread1 thread1 = new MyThread1();
MyThread2 thread2 = new MyThread2();
thread1.setName("thread1");
thread2.setName("thread2");
thread1.start();
thread2.start();
}
}
运行结果 :线程 1 遍历时,线程 2 修改集合,抛出 ConcurrentModificationException
。
fail-fast 的工作原理
fail-fast 的核心在于一个叫 modCount
的字段。
modCount
是集合类中的一个变量,用来记录集合被修改的次数。- 每次使用迭代器时,会把
modCount
的当前值保存到expectedModCount
。 - 在遍历过程中,迭代器会不断检查
modCount
和expectedModCount
是否一致。如果不一致,就抛出异常。
关键源码解析(以 ArrayList
为例)
java
private class Itr implements Iterator<E> {
int cursor; // 当前遍历位置
int lastRet = -1; // 上一次返回的元素索引
int expectedModCount = modCount; // 初始化时保存 modCount
public boolean hasNext() {
return cursor != size;
}
public E next() {
checkForComodification();
int i = cursor;
if (i >= size) throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
cursor = i + 1;
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
}
只要 modCount
被修改,checkForComodification()
就会抛出 ConcurrentModificationException
。
如何避免 fail-fast?
-
使用迭代器的
remove()
方法 :迭代器的remove()
方法会更新modCount
,避免异常。javapublic static void main(String[] args) { List<String> list = new ArrayList<>(); for (int i = 0; i < 10; i++) { list.add(i + ""); } Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { if (iterator.next().equals("3")) { iterator.remove(); // 安全移除 } } }
-
使用并发集合 :如
CopyOnWriteArrayList
或ConcurrentHashMap
,这些集合采用 fail-safe 机制。
fail-safe(安全失败)
fail-safe 的特点
- 定义:fail-safe 遍历操作时,是基于集合的副本进行的。
- 应用场景 :
java.util.concurrent
包下的并发集合(如CopyOnWriteArrayList
、ConcurrentHashMap
等)。 - 原理:遍历时对集合内容进行拷贝,因此对原集合的修改不会影响遍历过程,也不会触发异常。
fail-safe 的优缺点
- 优点 :避免了
ConcurrentModificationException
。 - 缺点:遍历的内容是副本,无法反映集合的最新状态;同时,拷贝集合会消耗更多内存。
示例:CopyOnWriteArrayList
java
public static void main(String[] args) {
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i + "");
}
for (String s : list) {
if (s.equals("3")) {
list.remove(s); // 不会触发异常
}
}
System.out.println(list);
}
总结
- fail-fast 是基于集合本体操作,检测到修改会抛异常,适用于单线程操作。
- fail-safe 是基于集合副本操作,允许修改,但无法反映最新状态,适用于并发环境。
如何选择?
- 单线程环境 :可以用 fail-fast,注意使用迭代器的
remove()
。 - 多线程环境 :优先选择 fail-safe,例如
ConcurrentHashMap
和CopyOnWriteArrayList
。
最后说一句(求关注,求赞,别白嫖我)
最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。
这是大佬写的 7701页的BAT大佬写的刷题笔记,让我offer拿到手软
本文,已收录于,我的技术网站 cxykk.com:程序员编程资料站,有大厂完整面经,工作技术,架构师成长之路,等经验分享