并发编程实战(6): fail-fast机制与ConcurrentModificationException
ConcurrentModificationException异常
java.util.ConcurrentModificationException是一个非常常见的异常,常见于对List、Map等集合的操作当中。那么,这个异常是什么呢?又为什么会产生这个异常呢?
随便打开ArrayList或者HashMap的源码(这里用的jdk1.8)可以看到,在其一个内部类迭代器的几乎所有方法实现中,都有对这个异常的抛出。
/**
* 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() {
...
throw new ConcurrentModificationException();
}
public void remove() {
...
throw new ConcurrentModificationException();
}
}
...
既然这个异常如此频繁的抛出,在编写代码过程成遇到这个问题也就不奇怪了,提醒我要重视这个异常。那么,它到底是什么呢?打开源码顶部作者注释,可以看到作者的解释(以ArrayList为例子):
* <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>
翻译过来就是,迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误的做法:迭代器的快速失败行为应该仅用于检测 bug。
介绍中出现了多次fail-fast机制,这个后面会介绍。这种“及时失败”的迭代器并不是一种完备的处理机制,可以看作只是一种“善意的”提醒,因此只能作为并发问题的预警指示器。在迭代器过程中,无法对集合进行修改(可以进行remove)。
Fail-Fast机制
“快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
说白了,fail-fast机制就是为了防止多线程修改集合造成并发问题的机制。
上面一直说是多线程环境下,也就是说是为了并发而准备的。但是要注意,单线程环境下也可能会出现问题,如下面的代码:
List<String> l = new ArrayList<>();
l.addAll(Arrays.asList("a", "b", "c", "d", "e"));
Iterator<String> it = l.iterator();
while(it.hasNext()){
String str = it.next();
if (str.equals("b"))
it.remove();
}
可以发现,在迭代的时候,只能进行remove操作,没有办法进行别的操作,比如add等。多线程环境下,remove就会报错了。
private static final int THREADNUM = 3;
public static void main(String[] args) {
List<String> l = new ArrayList<>();
l.addAll(Arrays.asList("a", "b", "c", "d", "e"));
ExecutorService es = Executors.newCachedThreadPool();
for (int i = 0; i < THREADNUM; i++) {
es.execute(() -> {
try {
Iterator<String> it = l.iterator();
while (it.hasNext()) {
String str = it.next();
System.out.println(str);
if (str.equals("b"))
it.remove();
}
} catch (Exception e){
e.printStackTrace();
}
});
}
}
从源码理解fail-fast产生原因
private class Itr implements Iterator<E> {
int cursor;
int lastRet = -1;
int expectedModCount = ArrayList.this.modCount;
public boolean hasNext() {
return (this.cursor != ArrayList.this.size);
}
public E next() {
checkForComodification();
/** 省略此处代码 */
}
public void remove() {
if (this.lastRet < 0)
throw new IllegalStateException();
checkForComodification();
/** 省略此处代码 */
}
final void checkForComodification() {
if (ArrayList.this.modCount == this.expectedModCount)
return;
throw new ConcurrentModificationException();
}
}
从源码中可以看出,迭代器中的方法都会调用checkForComodification()这个方法来进行检查。方法源码如下:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
可以看到,这个方法就是简单的对modCount和expectedModCount是否相同进行检查。如果两者不相等,就会抛出这个异常。另外,由于int expectedModCount = ArrayList.this.modCount; 所以,expectedModCount 这个值是不会变的,变得只有modeCount,那么它何时改变呢?
在AbstractList中定义了这个变量。
protected transient int modCount = 0;
看Arraylist源码可以知道,add、remove等方法都会对modCount进行增加。当expectedModCount 与modCount的改变不同步时,就会导致两者不相等,造成异常。 举例如下:
有两个线程(线程A,线程B),其中线程A负责遍历list、线程B修改list。线程A在遍历list过程的某个时候(此时expectedModCount = modCount=N),线程启动,同时线程B增加一个元素,这是modCount的值发生改变(modCount + 1 = N + 1)。线程A继续遍历执行next方法时,通告checkForComodification方法发现expectedModCount = N ,而modCount = N + 1,两者不等,这时就抛出ConcurrentModificationException 异常,从而产生fail-fast机制。
解决办法也有很多,后面学的深入了再做介绍。