起因
之前我是大概知道Java的快速失败机制的,但是没有认真看过源码,今天在看二哥的博客的时候发现其中的代码并没有快速失败,令我很是不解,特意去仔细了解了一下快速失败(fail-fast)和list的源码,并做了此文记录一下。
快速失败(fail-fast)
先了解一下源头:Java的快速失败(fail-fast)机制。
- 单线程
当在用迭代器或者增强for循环(增强for循环的底层也是迭代器)对一个集合进行遍历操作时,如果遍历的过程中集合的结构发生了变化,就会抛出并发修改异常ConcurrentModificationException
。 - 多线程
当一个线程在对一个集合进行遍历操作的时候,如果其他线程对这个集合的结构进行了修改,就会抛出并发修改异常ConcurrentModificationException
。
这就是Java的快速失败(fail-fast)机制。
注:集合的结构发生改变指的是增加或者删除元素,修改元素并不算结构的改变。
如何实现快速失败
实现的关键在于两个变量:modCount
和expectedModCount
。
其中,modCount
是ArrayList
类中定义的,代表集合结构被修改的次数,也就是说无论是调用集合本身还是迭代器的元素增加或者删除操作,使集合结构发生了变化,modCount
的值就会+1。
而expectedModCount
是ArrayList
类的迭代器Iterator
的内部实现类Itr
中定义的变量,其初始值为modCount
,如果集合结构变化是迭代器的remove()
方法导致的,这个变量的值就会+1,如果是集合本身的remove()
导致的,则值不会发生改变。
迭代器Itr
的next()
方法中首先会判断modCount
和expectedModCount
的值是否相等,如果不相等,则抛出ConcurrentModificationException
异常。
注:由于增强for循环的底层就是迭代器实现,因此每遍历到一个元素,就会执行迭代器的next()
方法。
快速失败的问题
List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
for (String s : list) {
System.out.println(s);
if (Objects.equals(s, "bbb")){
list.remove(s);
}
}
System.out.println(list);
上面这段代码很简单,在对集合的遍历过程中使用集合本身的remove()
方法对集合元素进行了删除,按照Java的快速失败机制,应该会抛出ConcurrentModificationException
异常,但是事实上,这段代码执行没有任何问题。
再看一下输出结果:
aaa
bbb
[aaa, ccc]
可以看出,集合中的"bbb"元素已经被删除,但是删除之后,集合的最后一个元素"ccc"并没有被遍历到,这又是怎么回事?
源码解释
下面是ArrayList
类的迭代器Iterator
的内部实现类Itr
源码:
private class Itr implements Iterator<E> {
int cursor; // 下一个要返回的元素的角标
int lastRet = -1; // 最后一个元素的角标,如果集合长度为0,则lastRet = -1
int expectedModCount = modCount;
public boolean hasNext() {
// 通过判断下一个元素的角标是否等于集合的大小来判断遍历过程中是否有下一个元素
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
// 遍历到当前元素的时候,会将cursor+1,使其指向下一个元素
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
// 迭代器的remove()方法执行结束之前会重新将modCount赋值给expectedModCount,
// 以保证不会触发快速失败机制。
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
// 快速失败机制
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
这里的关键在于cursor
这个变量,它表示当前元素的下一个元素的角标,而且每次在next()
方法执行结束之前会将这个值+1,这是正常情况。
那么,如果是使用迭代器对集合遍历过程中,使用了集合本身的remove()
方法,将当前元素删除,那么后面的元素依次前移,cursor
变量就会变成指向原集合中当前元素的下一个的下一个元素,此时如果继续遍历集合,再次执行迭代器的next()
方法,就会发现modCount
不等于expectedModCount
,也就会抛出ConcurrentModificationException
异常。
但是如果当前元素是集合中倒数第二个元素呢?此时原集合中当前元素的下一个的下一个元素不存在,而cursor
变量的值会变成集合的size
值,迭代器会以为集合已经遍历结束,从而中断遍历,也就不会再执行next()
方法,更不会抛出ConcurrentModificationException
异常。
这就是上面代码中为什么在增强for循环中使用了集合本身的remove()
方法使集合结构发生了变化但却没有抛出ConcurrentModificationException
异常,发生快速失败的原因。这样的话,输出结果中没有输出集合最后一个元素也是正常的,因为根本没有遍历到最后一个元素就提前结束了。
安全失败(fail-safe)
最后介绍一下安全失败(fail-safe),可能是无意中造成快速失败的人太多了,Java在java.util.concurrent
包中为我们提供了安全失败的集合类,如CopyOnWriteArrayList
等。其原理在于开始遍历之前会将原集合完整的复制一份,在复制的集合上进行遍历,在原集合上进行元素添加、删除操作,这样就可以避免了ConcurrentModificationException
异常。
当然,这类集合也有一些缺点:迭代器遍历的是开始遍历那一刻拿到的集合的拷贝,对于遍历过程中集合元素的变化,迭代器无法获取到。