Java程序在遍历一个非线程安全的List
或Map
时,因并发修改很容易出现ConcurrentModificationException
,根本原因是遍历时使用的iterator
是fail-fast的,也就是说,它认为并发修改是一种不被允许的异常情况,只要出现了就应该尽快失败。
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;
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();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
无论是显式地调用iterator()
还是通过for-each
遍历一个list
,都是通过上面的Itr
类,它会初始化一个expectedModCount
变量,这个变量后面不会变化,除非显式地通过Itr
类的remove()
方法来移除元素;在next()
和remove()
方法开始的地方,都会调用checkForComodification()
是否有并发修改,当modCount != expectedModCount
时就会抛出异常。
modCount
是在外围类AbstractList
中定义的变量,它记录了这个list
被结构性修改(指增加或者删除元素,文中所说的修改都是指结构性修改)的次数。当list
增加或者删除元素的时候,都会有modCount++
。例如下面的remove()
方法(注意这里和上面remove()
的区别是:不会改变iterator
的expectedModCount
):
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
综上所述:
- 像
ArrayList
,LinkedList
,HashMap
这些非线程安全的容器,在遍历时若有其他线程对容器做了结构性修改,都可能抛出ConcurrentModificationException
。 - 即使在同一个线程中,在遍历的同时如果调用容器的
remove()
方法来删除元素,也有可能抛出ConcurrentModificationException
,正确的做法是用iterator
的remove()
来删除。 - 设计这个异常的原因是:设计者认为对非线程安全的容器的并发修改是一种非法情况,出现了就应该立即失败。对于需要并发修改的情况,应该使用线程安全的容器。