在Java容器源码的注释中有这样一句话
The iterators returned by this class’s iterator and listIterator methods are fail-fast.
什么是fail-fast?
<p><a name="fail-fast">
* The iterators returned by this class's {@link #iterator() iterator} and
* {@link #listIterator(int) listIterator} methods are <em>fail-fast</em>:</a>
* if the list is structurally modified at any time after the iterator is
* created, in any way except through the iterator's own
* {@link ListIterator#remove() remove} or
* {@link ListIterator#add(Object) add} methods, the iterator will throw a
* {@link ConcurrentModificationException}. Thus, in the face of
* concurrent modification, the iterator fails quickly and cleanly, rather
* than risking arbitrary, non-deterministic behavior at an undetermined
* time in the future.
*
* <p>Note that the fail-fast behavior of an iterator cannot be guaranteed
* as it is, generally speaking, impossible to make any hard guarantees in the
* presence of unsynchronized concurrent modification. Fail-fast iterators
* throw {@code ConcurrentModificationException} on a best-effort basis.
* Therefore, it would be wrong to write a program that depended on this
* exception for its correctness: <i>the fail-fast behavior of iterators
* should be used only to detect bugs.</i>
大致翻译如下:
由iterator()和listIterator()返回的迭代器是fail-fast的。在于程序在对list进行迭代时,某个线程对该collection在结构上对其做了修改,这时迭代器就会抛出ConcurrentModificationException异常信息。因此,面对并发的修改,迭代器快速而干净利落地失败,而不是在不确定的情况下冒险。由elements()返回的Enumerations不是fail-fast的。需要注意的是,迭代器的fail-fast并不能得到保证,它不能够保证一定出现该错误。一般来说,fail-fast会尽最大努力抛出ConcurrentModificationException异常。因此,为提高此类操作的正确性而编写一个依赖于此异常的程序是错误的做法,正确做法是:ConcurrentModificationException应该仅用于检查bug。
大意为在遍历一个集合时,当集合结构被修改,很有可能会抛出ConcurrentModificationException。为什么说是很有可能呢?从下文中我们可以知道,迭代器的remove操作(注意是迭代器的remove方法而不是集合的remove方法)修改集合结构就不会导致这个异常。
看到这里我们就明白了,fail-fast机制是Java容器(Collection和Map都存在fail-fast机制)中的一种错误机制。在遍历一个容器对象时,当容器结构被修改,很有可能会抛出ConcurrentModificationException,产生fail-fast。
什么时候会出现fail-fast?
在以下两种情况下会导致fail-fast,抛出ConcurrentModificationException
遍历一个集合过程中,集合结构被修改。注意,listIterator.remove()方法修改集合结构不会抛出这个异常。
- 单线程环境
- 多线程环境
当一个线程遍历集合过程中,而另一个线程对集合结构进行了修改。
单线程环境例子
import java.util.ListIterator;
import java.util.Vector;
public class Test {
/**
* 单线程测试
*/
@org.junit.Test
public void test() {
try {
// 测试迭代器的remove方法修改集合结构会不会触发checkForComodification异常
ItrRemoveTest();
System.out.println("----分割线----");
// 测试集合的remove方法修改集合结构会不会触发checkForComodification异常
ListRemoveTest();
} catch (Exception e) {
e.printStackTrace();
}
}
// 测试迭代器的remove方法修改集合结构会不会触发checkForComodification异常
private void ItrRemoveTest() {
Vector list = new Vector<>();
list.add("1");
list.add("2");
list.add("3");
ListIterator itr = list.listIterator();
while (itr.hasNext()) {
System.out.println(itr.next());
//迭代器的remove方法修改集合结构
itr.remove();
}
}
// 测试集合的remove方法修改集合结构会不会触发checkForComodification异常
private void ListRemoveTest() {
Vector list = new Vector<>();
list.add("1");
list.add("2");
list.add("3");
ListIterator itr = list.listIterator();
while (itr.hasNext()) {
System.out.println(itr.next());
//集合的remove方法修改集合结构
list.remove("3");
}
}
}
运行结果
1
2
3
----分割线----
1
java.util.ConcurrentModificationException
at java.util.Vector$Itr.checkForComodification(Unknown Source)
从结果中可以看到迭代器itr的remove操作并没有出现ConcurrentModificationException异常。而集合的remove操作则产生了异常。
多线程环境例子
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Vector;
public class Test {
private static List<String> list = new Vector<String>();
/**
* 多线程情况测试
*/
@org.junit.Test
public void test2() {
list.add("1");
list.add("2");
list.add("3");
// 同时启动两个线程对list进行操作!
new ErgodicThread().start();
new ModifyThread().start();
}
/**
* 遍历集合的线程
*/
private static class ErgodicThread extends Thread {
public void run() {
int i = 0;
while (i < 10) {
printAll();
i++;
}
}
}
/**
* 修改集合的线程
*/
private static class ModifyThread extends Thread {
public void run() {
list.add(String.valueOf("5"));
}
}
/**
* 遍历集合
*/
private static void printAll() {
Iterator iter = list.iterator();
while (iter.hasNext()) {
System.out.print((String) iter.next() + ", ");
}
System.out.println();
}
}
运行结果
1, 2, 3,
1, 2, 3,
1, 2, 3,
1, Exception in thread "Thread-0" java.util.ConcurrentModificationException
at java.util.Vector$Itr.checkForComodification(Unknown Source)
从结果中可以看出当一个线程遍历集合,而另一个线程对这个集合的结构进行了修改,确实有可能触发ConcurrentModificationException异常。
fail-fast实现原理
/**
* An optimized version of AbstractList.Itr
*/
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
Itr() {}
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 = 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;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
其中,有三个属性:
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
cursor是指集合遍历过程中的即将遍历的元素的索引,lastRet是cursor -1,默认为-1,即不存在上一个时,为-1,它主要用于记录刚刚遍历过的元素的索引。expectedModCount这个就是fail-fast判断的关键变量了,它初始值就为ArrayList中的modCount。(modCount是抽象类AbstractList中的变量,默认为0,而ArrayList 继承了AbstractList ,所以也有这个变量,modCount用于记录集合操作过程中作的修改次数,与size还是有区别的,并不一定等于size)
我们一步一步来看:
public boolean hasNext() {
return cursor != size;
}
迭代器迭代结束的标志就是hasNext()返回false,而该方法就是用cursor游标和size(集合中的元素数目)进行对比,当cursor等于size时,表示已经遍历完成。
接下来看看最关心的next()方法,看看为什么在迭代过程中,如果有线程对集合结构做出改变,就会发生fail-fast:
@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 = i + 1;
return (E) elementData[lastRet = i];
}
从源码知道,每次调用next()方法,在实际访问元素前,都会调用checkForComodification方法,该方法源码如下:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
可以看出,该方法才是判断是否抛出ConcurrentModificationException异常的关键。在该段代码中,当modCount != expectedModCount时,就会抛出该异常。但是在一开始的时候,expectedModCount初始值默认等于modCount,为什么会出现modCount != expectedModCount,很明显expectedModCount在整个迭代过程除了一开始赋予初始值modCount外,并没有再发生改变,所以可能发生改变的就只有modCount,在前面关于ArrayList扩容机制的分析中,可以知道在ArrayList进行add,remove,clear等涉及到修改集合中的元素个数的操作时,modCount就会发生改变(modCount ++),所以当另一个线程(并发修改)或者同一个线程遍历过程中,调用相关方法使集合的个数发生改变,就会使modCount发生变化,这样在checkForComodification方法中就会抛出ConcurrentModificationException异常。
类似的,hashMap中发生的原理也是一样的。
使用迭代器的remove()方法修改集合结构不会触发ConcurrentModificationException,现在可以在源码中看出来是为什么。在remove()方法的最后会执行expectedModCount = modCount;,这样itr.remove操作后modCount和expectedModCount依然相等,就不会触发ConcurrentModificationException了。
如何避免fail-fast?
使用java.util.concurrent包下的类去取代java.util包下的类。所以,本例中只需要将Vector替换成java.util.concurrent包下对应的类即可。
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Vector;
import java.util.concurrent.CopyOnWriteArrayList;
public class Test {
/**
* CopyOnWriteArrayList的fail-fast测试
*/
@org.junit.Test
public void test3() {
try {
List list = new CopyOnWriteArrayList<>();
list.add("1");
list.add("2");
list.add("3");
ListIterator itr = list.listIterator();
while (itr.hasNext()) {
System.out.println(itr.next());
list.add("5");
list.remove("2");
}
System.out.println(list.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果
从运行结果中不难发现,在遍历过程中,使用集合的remove()方法修改集合结构并没有产生ConcurrentModificationException。