本文主要了解了Java中的快速失败机制和安全失败机制。

注意:本文基于JDK1.8进行记录。

1 快速失败机制

1.1 说明

快速失败机制,即fail-fast机制,直接在容器上进行遍历,在遍历过程中一旦发现集合的结构发生改变,就会抛出ConcurrentModificationException异常导致遍历失败。

java.util包下的集合类都是快速失败机制的,常见的的使用fail-fast方式遍历的容器有ArrayList和HashMap等。

fail-fast机制不能保证在不同步的修改下一定会抛出异常,它只是尽最大努力抛出,因此这种机制一般用于检测BUG。

1.2 现象

触发fail-fast机制的案例:

1 public static void main(String[] args) {
 2     List<String> list = new ArrayList<>();
 3     list.add("00");
 4     list.add("11");
 5     list.add("22");
 6     for (String s : list) {
 7         if ("00".equals(s)) {
 8             list.add("99");
 9         }
10     }
11 }

注意:此处只是列举了一个普通案例,实际上在List、Set、Map中都会发生,并且在单线程和多线程环境下都会发生。

1.3 原因

fail-fast机制在单线程和多线程环境中均可发生,倘若在迭代遍历过程中检测到集合结构有变化,就有可能触发并抛出异常。

想要理解fail-fast机制,就需要查看底层源码的逻辑,因为引发fail-fast机制的原理是一样的,本文以ArrayList为例进行分析。

查看ArrayList的迭代器方法:

1 public Iterator<E> iterator() {
2     return new Itr();
3 }

继续查看ArrayList维护的内部类Itr,需要重点关注三个属性:

1 private class Itr implements Iterator<E> {
 2     int cursor;       // 遍历集合时即将遍历的索引
 3     int lastRet = -1; // 记录刚刚遍历的索引,-1不是不存在上一个元素
 4     int expectedModCount = modCount;// 初始值为modCount,用于记录集合的修改次数
 5 
 6     public boolean hasNext() {
 7         return cursor != size;// 判断遍历是否结束
 8     }
 9 
10     @SuppressWarnings("unchecked")
11     public E next() {
12         checkForComodification();// 检查是否触发fail-fast机制
13         int i = cursor;
14         if (i >= size)
15             throw new NoSuchElementException();
16         Object[] elementData = ArrayList.this.elementData;
17         if (i >= elementData.length)
18             throw new ConcurrentModificationException();
19         cursor = i + 1;
20         return (E) elementData[lastRet = i];
21     }
22     ...
23     final void checkForComodification() {
24         if (modCount != expectedModCount)
25             // expectedModCount在初始化后并未发生改变,那么如果modCount发生改变,就会抛出异常
26             throw new ConcurrentModificationException();
27     }
28 }

通过分析迭代器源码可以发现,迭代器的checkForComodification方法是判断是否要触发fail-fast机制的关键。

在checkForComodification方法中可以看到,是否要抛出异常在于modCount是否发生改变。

查看ArrayList源码,发现modCount的改变发生在对集合修改中,比如add操作。

所以当在使用迭代器遍历集合时,如果同时对集合进行了修改,导致modCount发生改变,就会触发fail-fast机制,抛出异常。

1.4 解决

1.4.1 使用迭代器提供的方法

为了避免触发fail-fast机制,在迭代集合时,需要使用迭代器提供的修改方法修改集合。

1.4.2 使用线程安全的集合类

也可以使用线程安全的集合类,使用CopyOnWriteArrayList代替ArrayList,使用ConcerrentHashMap代替HashMap。

2 安全失败机制

2.1 说明

安全失败机制,即fail-safe机制,在集合的克隆对象上进行遍历,对集合的修改不会影响遍历操作。

java.util.concurrent包下的集合类都是安全失败的,可以在多线程下并发使用并发修改,常见的的使用fail-safe方式遍历的容器有CopyOnWriteArrayList和ConcerrentHashMap等。

基于克隆对象的遍历避免了在修改集合时抛出ConcurrentModificationException异常,但同样导致遍历时不能访问修改后的内容。