Java·ConcurrentModificationException的具體原因
《阿里巴巴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
是偶爾出現:
斷點除錯
設定斷點
我在這三處地方都打了斷點,這樣我們就能大概清楚整個流程:


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

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

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

圈紅1
注意我圈紅的第一處地方,我們進入checkForComodification裡:
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } 複製程式碼
可以看到,這就是我們報錯的關鍵點,這裡的 modCount
變數是指集合被操作的次數,比如像 add()
、 remove()
這些方法都會讓 modCount + 1
,而 expectedModCount
是指集合的一個預期操作次數,在部分操作裡會被重置為 modCount
,比如 add()
方法裡。
因為我們上面添加了兩個元素,所以 modCount
和 modCount
都是2。
圈紅2
接著我們看第二處圈紅的地方,我們可以發現,每一次 next()
的時候指標都會移動,這很好理解。
P2
斷點繼續,因為第一個元素就是1,所以這裡匹配上了:

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

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

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

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


吃鯨
這裡你是不是懵逼了,咦?說好的報錯呢?怎麼沒報錯了?
咳咳,其實是因為有時候會出現像上面這種 巧合 的情況, 就是在 hasNext()
方法校驗的時候, cursor
剛好不等於 size
,然後就退出了 ,而剛好集合又遍歷完了,but,這個情況是很少出現的,一般都會丟擲 ConcurrentModificationException
異常,所以大家不要有僥倖的心理。
還原報錯
下面我們還是以上面的例子,只是這次我把刪除的物件 從1改為2 :

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

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

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

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

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