注意:本文基于JDK1.8进行记录。
1 快速失败机制
1.1 说明
快速失败机制,即fail-fast机制,直接在容器上进行遍历,在遍历过程中一旦发现集合的结构发生改变,就会抛出ConcurrentModificationException异常导致遍历失败。
java.util包下的集合类都是快速失败机制的,常见的的使用fail-fast方式遍历的容器有ArrayList和HashMap等。
fail-fast机制不能保证在不同步的修改下一定会抛出异常,它只是尽最大努力抛出,因此这种机制一般用于检测BUG。
1.2 现象
触发fail-fast机制的案例:
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("00");
list.add("11");
list.add("22");
for (String s : list) {
if ("00".equals(s)) {
list.add("99");
}
}
}
注意:此处只是列举了一个普通案例,实际上在List、Set、Map中都会发生,并且在单线程和多线程环境下都会发生。
1.3 原因
fail-fast机制在单线程和多线程环境中均可发生,倘若在迭代遍历过程中检测到集合结构有变化,就有可能触发并抛出异常。
想要理解fail-fast机制,就需要查看底层源码的逻辑,因为引发fail-fast机制的原理是一样的,本文以ArrayList为例进行分析。
查看ArrayList的迭代器方法:
public Iterator<E> iterator() {
return new Itr();
}
继续查看ArrayList维护的内部类Itr,需要重点关注三个属性:
private class Itr implements Iterator<E> {
int cursor; // 遍历集合时即将遍历的索引
int lastRet = -1; // 记录刚刚遍历的索引,-1不是不存在上一个元素
int expectedModCount = modCount;// 初始值为modCount,用于记录集合的修改次数
public boolean hasNext() {
return cursor != size;// 判断遍历是否结束
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();// 检查是否触发fail-fast机制
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
...
final void checkForComodification() {
if (modCount != expectedModCount)
// expectedModCount在初始化后并未发生改变,那么如果modCount发生改变,就会抛出异常
throw new ConcurrentModificationException();
}
}
通过分析迭代器源码可以发现,迭代器的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异常,但同样导致遍历时不能访问修改后的内容。