《阿里巴巴Java開發手冊》JavaConcurrentModificationException

《阿里巴巴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);
}
}
/<string>

其實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);
}
}
/<string>/<string>

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

public Iterator iterator() {
return listIterator();
}
public ListIterator listIterator(final int index) {
checkForComodification();
rangeCheckForAdd(index);

final int offset = this.offset;
return new ListIterator() {
hasNext()...
next()...
...
}
}

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

斷點調試

設置斷點

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

《阿里巴巴Java開發手冊》JavaConcurrentModificationException

《阿里巴巴Java開發手冊》JavaConcurrentModificationException

運行調試

P1

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

《阿里巴巴Java開發手冊》JavaConcurrentModificationException

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

《阿里巴巴Java開發手冊》JavaConcurrentModificationException

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

《阿里巴巴Java開發手冊》JavaConcurrentModificationException

圈紅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,所以這裡匹配上了:

《阿里巴巴Java開發手冊》JavaConcurrentModificationException

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

《阿里巴巴Java開發手冊》JavaConcurrentModificationException

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

《阿里巴巴Java開發手冊》JavaConcurrentModificationException

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

《阿里巴巴Java開發手冊》JavaConcurrentModificationException

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

《阿里巴巴Java開發手冊》JavaConcurrentModificationException

《阿里巴巴Java開發手冊》JavaConcurrentModificationException

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

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

還原報錯

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

《阿里巴巴Java開發手冊》JavaConcurrentModificationException

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

《阿里巴巴Java開發手冊》JavaConcurrentModificationException

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

《阿里巴巴Java開發手冊》JavaConcurrentModificationException

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

《阿里巴巴Java開發手冊》JavaConcurrentModificationException

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

《阿里巴巴Java開發手冊》JavaConcurrentModificationException

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

《阿里巴巴Java開發手冊》JavaConcurrentModificationException

關注我:私信回覆“555”獲取往期Java高級架構資料、源碼、筆記、視頻Dubbo、Redis、Netty、zookeeper、Spring cloud、分佈式、高併發等架構技術往期架構視頻截圖


分享到:


相關文章: