在 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 {@Overridepublic 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;@Overridepublic 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; // 初始化时保存 modCountpublic 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
,避免异常。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();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:程序员编程资料站,有大厂完整面经,工作技术,架构师成长之路,等经验分享