关于ArrayList的ConcurrentModificationException的一些思考
先来看一个来自于阿里java规范文档的例子:
List<String> a = new ArrayList<String>();
a.add("1");
a.add("2");
for (String temp : a) {
if("1".equals(temp)){
a.remove(temp);
}
}
这里的执行结果比较奇怪。(待会再分析)
分析ArrayList的源码,可以看到其(或者父类AbstractList)维护一个modCount的变量:
protected transient int modCount = 0;
根据文档,这个变量的作用是
The number of times this list has been <i>structurally modified</i>.
Structural modifications are those that change the size of the list, or otherwise perturb it in such a fashion that iterations in progress may yield incorrect results.
这个数字的描述的是list的结构性修改次数,结构性修改指的是,那些会改变list的长度或者破坏list的方式,使得正在进行的迭代产生错误结果。
我们来看看ArrayList中为什么会抛出ConcurrentModificationException:
- I remove()方法(改变modCount值)
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;
}
以及add()方法
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;//look这里
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
可以看出,remove()的时候是对modCount进行过修改,同样的在add()方法中也是,即添加删除多少次modCount就是多少。我们并没有看到ConcurrentModificationException的抛出,说明,无论我们怎么对ArrayList进行修改,只会改变modCount值而不会抛出异常。
那么是哪里抛出的呢,看下面:
- II 迭代器Itr(抛出ConcurrentModificationException异常)
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;//获取迭代器的时候会初始化expectedModCount
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();//检查list是否被操作过,若没有继续
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
//数据没有被操作而合法的游标cursor又大于数组的长度,日了狗了,hasNext()为什么不用return cursor < size
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();
}
}
从上面的代码可以看出在迭代器Itr的next(), remove()这两个方法在进行操作前都会去使用checkForComodification校验modCount是否一致,即保证当前操作前的list没有被改变,就像每次出门进门都会检查锁坏没有一样,若没有坏(modCount == expectedModCount)就继续开门锁门,若坏了(modCount != expectedModCount),就报警或者找修锁的(抛出ConcurrentModificationException)。
所以在多线程环境中,一个线程要添加删除,另一个线程迭代,很容易中招。
回到最开始的例子,这个例子可以正常的执行下去,最后a里只剩一个元素’2’。但是啊但是,若改成
...
if(“2”.equals(temp)) {
//......
}
...
此时运行会抛出ConcurrentModificationException,这跟我们刚刚分析的不一样啊,明明没有用迭代器,怎么会抛出ConcurrentModificationException。
没办法,看看异常信息吧,如下:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at cc.rinoux.instantTest.main(instantTest.java:16)
......
果然还是迭代器产生的异常。什么时候调的迭代器?当然是foreach语句,foreach语句实际上是对iterator.hasNext()和next()的调用,先判断hasNext(),为true再next()获得元素(并非语法糖,涉及到Collection和Iterable,此处不赘述)。
但是为什么if(“1”.equals(temp))没有抛异常,而if(“2”.equals(temp))抛异常,这就涉及到上面注释里写的hasNext()为什么要这样写。
前者在完成第一次next操作后,cursor变为1,在remove,size变为1;继续遍历,先做hasNext判断,此时size为1 == cursor,直接返回false跳出循环遍历中止,因此没机会去抛出 ConcurrentModificationException。
后者则是,第一个遍历结果,不满足remove条件,因此modCount依然为0,hasNext()依然为true;第二个遍历结果满足,remove,modCount为1, size为1, cursor为2;在下一次foreach中,hasNext()通过,但是next()就不行了,因为modCount != expectedModCount,所以操作前检查抛出ConcurrentModificationException。
注意这里,cursor是大于size的,游标大于list长度的情况就发生了,所以hasNext()用的return cursor != size 而非 return cursor < size。