關(guān)于ArrayList的ConcurrentModificationException的一些思考
先來(lái)看一個(gè)來(lái)自于阿里java規(guī)范文檔的例子:
List<String> a = new ArrayList<String>();
a.add("1");
a.add("2");
for (String temp : a) {
if("1".equals(temp)){
a.remove(temp);
}
}
這里的執(zhí)行結(jié)果比較奇怪。(待會(huì)再分析)
分析ArrayList的源碼逼纸,可以看到其(或者父類(lèi)AbstractList)維護(hù)一個(gè)modCount的變量:
protected transient int modCount = 0;
根據(jù)文檔音同,這個(gè)變量的作用是
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.
這個(gè)數(shù)字的描述的是list的結(jié)構(gòu)性修改次數(shù)踪宠,結(jié)構(gòu)性修改指的是鹃共,那些會(huì)改變list的長(zhǎng)度或者破壞list的方式腺逛,使得正在進(jìn)行的迭代產(chǎn)生錯(cuò)誤結(jié)果唤冈。
我們來(lái)看看ArrayList中為什么會(huì)拋出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()的時(shí)候是對(duì)modCount進(jìn)行過(guò)修改颇象,同樣的在add()方法中也是,即添加刪除多少次modCount就是多少并徘。我們并沒(méi)有看到ConcurrentModificationException的拋出遣钳,說(shuō)明,無(wú)論我們?cè)趺磳?duì)ArrayList進(jìn)行修改麦乞,只會(huì)改變modCount值而不會(huì)拋出異常蕴茴。
那么是哪里拋出的呢,看下面:
- 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;//獲取迭代器的時(shí)候會(huì)初始化expectedModCount
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();//檢查list是否被操作過(guò)姐直,若沒(méi)有繼續(xù)
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
//數(shù)據(jù)沒(méi)有被操作而合法的游標(biāo)cursor又大于數(shù)組的長(zhǎng)度倦淀,日了狗了,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()這兩個(gè)方法在進(jìn)行操作前都會(huì)去使用checkForComodification校驗(yàn)modCount是否一致晃听,即保證當(dāng)前操作前的list沒(méi)有被改變,就像每次出門(mén)進(jìn)門(mén)都會(huì)檢查鎖壞沒(méi)有一樣砰识,若沒(méi)有壞(modCount == expectedModCount)就繼續(xù)開(kāi)門(mén)鎖門(mén)能扒,若壞了(modCount != expectedModCount),就報(bào)警或者找修鎖的(拋出ConcurrentModificationException)辫狼。
所以在多線程環(huán)境中初斑,一個(gè)線程要添加刪除,另一個(gè)線程迭代膨处,很容易中招见秤。
回到最開(kāi)始的例子,這個(gè)例子可以正常的執(zhí)行下去真椿,最后a里只剩一個(gè)元素’2’鹃答。但是啊但是,若改成
...
if(“2”.equals(temp)) {
//......
}
...
此時(shí)運(yùn)行會(huì)拋出ConcurrentModificationException突硝,這跟我們剛剛分析的不一樣啊测摔,明明沒(méi)有用迭代器,怎么會(huì)拋出ConcurrentModificationException解恰。
沒(méi)辦法锋八,看看異常信息吧,如下:
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)
......
果然還是迭代器產(chǎn)生的異常护盈。什么時(shí)候調(diào)的迭代器挟纱?當(dāng)然是foreach語(yǔ)句,foreach語(yǔ)句實(shí)際上是對(duì)iterator.hasNext()和next()的調(diào)用腐宋,先判斷hasNext()紊服,為true再next()獲得元素(并非語(yǔ)法糖檀轨,涉及到Collection和Iterable,此處不贅述)围苫。
但是為什么if(“1”.equals(temp))沒(méi)有拋異常裤园,而if(“2”.equals(temp))拋異常,這就涉及到上面注釋里寫(xiě)的hasNext()為什么要這樣寫(xiě)剂府。
前者在完成第一次next操作后拧揽,cursor變?yōu)?,在remove腺占,size變?yōu)?淤袜;繼續(xù)遍歷,先做hasNext判斷衰伯,此時(shí)size為1 == cursor铡羡,直接返回false跳出循環(huán)遍歷中止,因此沒(méi)機(jī)會(huì)去拋出 ConcurrentModificationException意鲸。
后者則是烦周,第一個(gè)遍歷結(jié)果,不滿足remove條件怎顾,因此modCount依然為0读慎,hasNext()依然為true;第二個(gè)遍歷結(jié)果滿足槐雾,remove夭委,modCount為1, size為1募强, cursor為2株灸;在下一次foreach中,hasNext()通過(guò)擎值,但是next()就不行了慌烧,因?yàn)閙odCount != expectedModCount,所以操作前檢查拋出ConcurrentModificationException鸠儿。
注意這里杏死,cursor是大于size的,游標(biāo)大于list長(zhǎng)度的情況就發(fā)生了捆交,所以hasNext()用的return cursor != size 而非 return cursor < size。