Java·ConcurrentModificationException的具體原因

super2god發表於2019-02-06

《阿里巴巴Java開發手冊》第一章裡的第五節的第七點是這麼說的:

【強制】不要在 foreach 迴圈裡進行元素的 remove/add 操作。remove 元素請使用 Iterator 方式,如果併發操作,需要對 Iterator 物件加鎖。

裡面舉了這樣一個反例:

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");

for (String item : list) {
  if ("1".equals(item)) {
    list.remove(item);
  }
} 
複製程式碼

其實Java的forEach寫法內部就是迭代器,大家可以把上面的程式碼理解為以下程式碼:

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
  String item = iterator.next();
  if ("1".equals(item)) {
    list.remove(item);
  }
}
複製程式碼

有了這一層理解後,那我們以ArrayList為例,看看其內部的iterator方法:

public Iterator<E> iterator() {
	return listIterator();
}

public ListIterator<E> listIterator(final int index) {
            checkForComodification();
            rangeCheckForAdd(index);
            final int offset = this.offset;

            return new ListIterator<E>() {
              hasNext()...
              next()...
            ...
						}
}             
複製程式碼

由於listIterator()方法內的內部類ListIterator的程式碼太多,我就不一一貼出來了,因為我們重點只看兩個方法:hasNext()next(),接下來我會通過斷點除錯讓大家明白為什ConcurrentModificationException是偶爾出現:

斷點除錯

設定斷點

我在這三處地方都打了斷點,這樣我們就能大概清楚整個流程:

image.png
image.png

執行除錯

P1

好的,我們看到已經定位到第一個斷點位置了,從idea提供的資訊我們也可以看出list的大小為2:

image.png

接著往下走,又來到了第二個斷點的位置,在上面我已經說了forEach的語法的原理了,所以這樣會走到haxNext()函式這裡,這裡的cursor是指當前迭代器的指標,而size是當前集合的大小:

image.png

繼續走,我們會來到第三個斷點:

image.png

圈紅1

注意我圈紅的第一處地方,我們進入checkForComodification裡:

final void checkForComodification() {
  if (modCount != expectedModCount)
    throw new ConcurrentModificationException();
}
複製程式碼

可以看到,這就是我們報錯的關鍵點,這裡的modCount變數是指集合被操作的次數,比如像add()remove()這些方法都會讓modCount + 1,而expectedModCount是指集合的一個預期操作次數,在部分操作裡會被重置為modCount,比如add()方法裡。

因為我們上面新增了兩個元素,所以modCountmodCount都是2。

圈紅2

接著我們看第二處圈紅的地方,我們可以發現,每一次next()的時候指標都會移動,這很好理解。

P2

斷點繼續,因為第一個元素就是1,所以這裡匹配上了:

image.png

我們進入到remove()方法裡面,因為我們是按照物件刪除的,所以會進入第二個分支:

image.png

接著我們再進入fastRemove()方法,可以看到modCount + 1了:

image.png

P3

繼續往下走,我們又回到最開始的地方,但仔細點你會發現list的大小從2變成1了:

image.png

然後我們又來到了hasNext()這裡了,因為cursorsize都是1,所以迴圈就終止了:

image.png
image.png

吃鯨

這裡你是不是懵逼了,咦?說好的報錯呢?怎麼沒報錯了?

咳咳,其實是因為有時候會出現像上面這種巧合的情況,就是在hasNext()方法校驗的時候,cursor剛好不等於size,然後就退出了,而剛好集合又遍歷完了,but,這個情況是很少出現的,一般都會丟擲ConcurrentModificationException異常,所以大家不要有僥倖的心理。

還原報錯

下面我們還是以上面的例子,只是這次我把刪除的物件從1改為2

image.png

執行除錯後跟上面的P1和P2是一樣的,所以這裡我就不重複了,唯一不同的地方在P3。這裡我們已經來到第二次迴圈,校驗元素後會刪除元素2:

image.png

在第三次迴圈,(這裡是指第三次進行hasNext())的時候,我們可以看到list的大小是1了:

image.png

ok,我們繼續往下,這裡大家要特別注意,可以看到cursor此時是2,而size卻是1,所以迴圈還可以繼續

image.png

前面我們說過next()方法裡的checkForComodification()是檢查操作次數的,所以這裡就不復述了:

image.png

我們進入到checkForComodification()裡,可以看到modCount是3(因為remove()操作**modCount**自增了),而expectedModCount是2,所以就報錯了

image.png

相關文章