👉本文章全部文字純原創,若是須要轉載,請註明轉載出處,謝謝!😘java
之因此今天想寫這篇文章徹底是一個偶然的機會。昨晚,微信技術羣裏的一位猿友@我,問了我一個問題,代碼以下。他問我,這樣寫有沒有問題,會不會報錯?而後他說這時他今天去面試的面試官出的題目,他回答不出來。😅git
public class CollectionDemo {
public static void main(String[] args) {
List list = new ArrayList();
list.add("1");
list.add("3");
list.add("5");
for (Object o : list) {
if ("3".equals(o))
list.remove(o);
}
System.out.println(list);
}
}
複製代碼
我當時沒仔細想,感受挺簡單的問題,😏但定睛一看,這個是在一個加強for循環中執行了一個list的remove方法。有點Java基礎的基友們確定都知道,用迭代器方式遍歷集合元素時,若是須要刪除或者修改集合中元素,必需要使用迭代器的方法,絕對不能使用集合自身的方法。我也一直把這句話視爲鐵律。因而我判定,這個代碼是有問題的,確定會報錯的。而後我噼裏啪啦一頓操做猛如虎,把這段代碼敲了一遍,一頓運行......輸出結果以下:github
結果傻眼了,竟然正常輸出沒有報錯,並且結果仍是正確的!因而我又改動了一下代碼,以下:面試
public class CollectionDemo {
public static void main(String[] args) {
List list = new ArrayList();
list.add("1");
//其他代碼都沒有修改,就在list.add("3");以前添加這一行
list.add("2");
list.add("3");
list.add("5");
for (Object o : list) {
if ("3".equals(o))
list.remove(o);
}
System.out.println(list);
}
}
複製代碼
輸出結果以下:後端
發現結果仍是正確的。微信
因而我又改動了一下代碼,以下:多線程
public class CollectionDemo {
public static void main(String[] args) {
List list = new ArrayList();
list.add("1");
list.add("2");
list.add("3");
//其他代碼都沒有修改,就在list.add("3")以後添加這一行
list.add("4");
list.add("5");
for (Object o : list) {
if ("3".equals(o))
list.remove(o);
}
System.out.println(list);
}
}
複製代碼
輸出結果以下:併發
這一次終於出現了期待已久的報錯。ide
真的是奇哉怪也,竟然會有如此不一樣的運行結果?! 這下讓我意識到了問題的嚴重性,這個問題並無之前理解的那麼簡單。再加上本身打破砂鍋問到底的性格,因而決定好好來研究一番,順便寫點東西,一方面本身之後能夠回顧,也能夠和各大佬交流技術,不亦樂乎?😎源碼分析
追根溯源,既然程序拋出該異常,那麼固然先要把這個異常搞清楚。秉承學技術一看官方文檔二看源碼的習慣,我就看了一下ConcurrentModificationException的javadoc,原文很是長,這邊貼一部分關鍵的,有興趣的能夠本身去翻閱JDK源碼。
/** * This exception may be thrown by methods that have detected concurrent * modification of an object when such modification is not permissible. * For example, it is not generally permissible for one thread to modify a Collection * while another thread is iterating over it.Some Iterator * implementations (including those of all the general purpose collection implementations * provided by the JRE) may choose to throw this exception if this behavior is * detected. Iterators that do this are known as <i>fail-fast</i> iterators, * as they fail quickly and cleanly, rather that risking arbitrary, * non-deterministic behavior at an undetermined time in the future. * Note that this exception does not always indicate that an object has * been concurrently modified by a <i>different</i> thread. If a single * thread issues a sequence of method invocations that violates the * contract of an object, the object may throw this exception. For * example, if a thread modifies a collection directly while it is * iterating over the collection with a fail-fast iterator, the iterator * will throw this exception. */
複製代碼
這一大段話大概意思是說,這個異常可能會在檢測到一個對象被作了不合法的併發修改,好比jdk自帶的集合一般會內置一個fail-fast類型的迭代器,當集合檢測到這類不合法的併發修改,就會拋出該異常。所謂的fail-fast,顧名思義,就是當檢測到有異常時,越快拋出異常結束越好,以避免未來帶來未知的隱患。另外這段話還說了,這個異常並非像名字那樣只會出如今多線程併發修改的狀況下,在單線程下也會出現。
然並卵,看了半天文檔仍是一臉懵逼。這到底說的是什麼鬼?
不要緊,控制檯除了拋出這個異常,還提示了具體的異常拋出的位置,在java.util.ArrayList$Itr.next()
內部的checkForComodification()
方法。定位到ArrayList源碼指定位置,以下圖標識紅框位置:
這個方法的邏輯很是簡單。
那modCount和expectedModCount又是何方神聖?跟着來到定義他們的地方。
modCount是定義在AbstractList(ArrayList的父類)裏面的一個屬性。這個屬性的javadoc也是至關長,我挑選一部分給你們看一下。
/** * 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. * <p><b>Use of this field by subclasses is optional.</b> If a subclass * wishes to provide fail-fast iterators (and list iterators), then it * merely has to increment this field in its {@code add(int, E)} and * {@code remove(int)} methods (and any other methods that it overrides * that result in structural modifications to the list). A single call to * {@code add(int, E)} or {@code remove(int)} must add no more than * one to this field, or the iterators (and list iterators) will throw * bogus {@code ConcurrentModificationExceptions}. If an implementation * does not wish to provide fail-fast iterators, this field may be * ignored. */
protected transient int modCount = 0;
複製代碼
大概意思是,這個字段的值是用來記錄list被結構性操做的次數。何爲結構性操做?就是對List的容量有影響的或者迭代過程當中會致使錯誤結果的操做。而子類可使用這個字段的值來實現fail-fast。若是要實現fail-fast,須要在全部結構性操做的方法內部作modCount++
操做,而且每一個方法內部只能增長一次。若是不想實現fail-fast就不須要這個值的。好比ArrayList的add方法裏面就有modCount++
操做,以下圖所示:
再來看看expectedModCount。expectedModCount是定義在java.util.ArrayList$Itr
裏面的屬性,而且會將ArrayList的modCount的值做爲其初始化值。
看到這裏是否是有點感受了?也就是正常狀況下,ArrayList初始化後,內置的Itr也跟着初始化,而且expectedModCount和modCount是保持一致的。若是沒有進行迭代操做,天然是不會出現不一致的問題,也就不會拋出ConcurrentModificationException。那咱們的程序到底爲何會致使這兩個值不一致呢?此時,不使用大招——debug我反正是機關用盡了。由於咱們的程序中使用了一個加強forEach循環,其實forEach能夠看作是jdk一個語法糖,底層就是使用迭代器實現的。因此爲了看清楚,咱們在java.util.ArrayList$Itr
的方法上都加上斷點。以下圖:
咱們就以開頭的那三個例子最後一個報錯的爲例,開始debug。
剛開始list添加了5個元素,size等於5。由前面得知,add操做屬於結構性操做,會致使modCount++
。
Itr迭代器的遊標cursor值會從0開始隨着元素的遍歷移動。hasNext()經過判斷cursor != size
來肯定list是否還有下一個元素取出。若是返回true,則會進入next()用來返回下一個元素。
顯然咱們有5個元素,能夠進入next()。而在next方法中,第一行代碼就是checkForComodification()用來校驗expectedModCount和modCount的一致性。顯然從List添加完元素到如今爲止,咱們沒有再對list有過額外的結構性操做,天然前面3次迭代都不會拋出異常,正常返回元素。都如圖所示。
而且每次執行完next()後,cursor會日後移動一位,爲迭代下一個元素作準備。
這個時候輪到迭代第三個元素"3"了。天然if條件判斷成立,會進入刪除操做。
跟進remove()方法源碼中,確實發現了modCount++
。也就是說,這個時候modCount值已經變成6了。而expectedModCount依然仍是保存着初始值5。此時二者不一致了。
由於list在「3」以後還有「4」,「5」兩個元素,所以當刪除「3」元素以後,迭代器還會繼續迭代,重複以前的流程,會先進入hasNext(),此時cursor等於3,size等於4,天然仍是知足的,因此仍是會繼續進入next()取出下一個元素。
能夠預料此時checkForComodification()校驗expectedModCount和modCount已經不一致了,因此拋出了ConcurrentModificationException。
也就是說,在forEach或者迭代器中調用對集合的結構性操做會致使modCount值發生修改,而expectedModCount的值仍然是初始化值,因此在next()中校驗不經過拋出異常。這也是爲何之前剛學習迭代器的時候,各大佬叫我不要在迭代器迭代過程當中使用集合自帶的remove等操做,而要使用迭代器自帶的remove方法,緣由就在於此。那爲何使用迭代器自帶的remove方法就不會報錯呢?以下代碼:
public class CollectionDemo {
public static void main(String[] args) {
List list = new ArrayList();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
list.add("5");
for (Iterator it = list.iterator(); it.hasNext(); ) {
if ("3".equals(it.next()))
it.remove();
}
System.out.println(list);
}
}
複製代碼
這是老師教的正確姿式。結果固然是正確的。
要搞清楚這中間的區別,固然仍是須要深刻虎穴,再去看看List迭代器remove方法的源碼了。下面代碼中主要關注紅框的2行,第一行做用是刪除被迭代的元素,ArrayList.this.remove
這個是調用外部類ArrayList的remove方法,上面已經說過了,集合的remove方法是結構性操做,會致使modCount++的,這樣等迭代下一個元素時,調用next()時校驗expectedModCount和modCount一致性必然會報錯,爲了防止這個問題,因此下一行expectedModCount = modCount
將expectedModCount更新至modCount最新值,使得一致性不被破壞。這也是爲何使用迭代器自帶的remove方法並不會拋出異常的緣由。
怎麼樣?是否是感受大功告成了,感受本身要飄了......
然而,這只是解釋了文章開頭3個例子的最後一個,那爲何前兩個能夠正常刪除沒有報錯?說實話,我當時遇到這問題的心裏是崩潰到懷疑人生的。
仍是沒有好的辦法,繼續來debug一下前面的例子,看看會有什麼不一樣的事情發生吧。
List中前面的元素的遍歷過程和上面是同樣的,再也不贅述。直接看關鍵處,以下圖,這個時候已經遍歷到「3」這個元素了,即將開始remove操做,remove操做也和上面同樣,會調用fastRemove()刪除元素,fastRemove()也確實會執行modCount++
,確實致使了expectedModCount != modCount
。可是......
當將要迭代下一個元素的時候,仍是會進入hashNext()作判斷,很遺憾,這個時候cursor和size都是2,也就是hashNext()條件不成立返回false,也就不會再進入next()方法,天然也就不會再去調用checkForComodification()作校驗,也就不會再有機會拋異常了。其實這個時候,list中最後一個元素"5"根本也就沒遍歷到。爲了驗證這一點,能夠在for循環中添加輸出代碼:
public class CollectionDemo {
public static void main(String[] args) {
List list = new ArrayList();
list.add("1");
list.add("3");
list.add("5");
for (Object o : list) {
System.out.println(o);//輸出正在迭代的元素
if ("3".equals(o))
list.remove(o);
}
System.out.println(list);
}
}
複製代碼
會發現只會輸出1和3。
事情還沒完,最後再來一種狀況,代碼以下:
public class CollectionDemo {
public static void main(String[] args) {
List list = new ArrayList();
list.add("1");
list.add("2");
list.add("3");
for (Object o : list) {
if ("3".equals(o))
list.remove(o);
}
System.out.println(list);
}
}
複製代碼
猜猜結果是啥?有人會認爲,不是和文章第一個例子如出一轍的嗎?那就是成功刪除了啊,輸出1和2啊。呵呵🙄,讓您失望了。
是否是又懷疑人生了?其實有了前面這麼多的鋪墊,這個錯誤緣由已經不難推斷髮現了。
緣由仍是在這裏。前面「1」,「2」兩個元素遍歷完畢確定是沒問題的,當開始遍歷「3」時候,經過next()返回元素「3」,cursor此時會增長到3,而size因爲後面會調用remove減爲2了,這個時候hasNext()裏的條件返回true又成立啦!個人乖乖......因此Itr迭代器又會傻傻的去調用next(),後面的事情就都知道了,checkForComodification()又被調用了,拋出ConcurrentModificationException異常。
其實經過上述的整個分析過程,能夠總結出一點結論:其實整個過程的問題關鍵所在就是java.util.ArrayList$Itr
的hasNext()方法的邏輯。不難看出,每當迭代器返回一個元素時,元素在列表中的索引等於Itr的cursor值,而每次刪除一個元素會致使size--
,不難推斷出,若是你要刪除的元素剛好位於List倒數第二個位置,則並不會拋出異常,而且會顯示正確的刪除操做,就像文章開頭第一個例子,其他狀況都會拋出異常。可是就算是不拋異常的狀況,其實此時List迭代器內部的expectedModCount 和modCount一致性已經遭到了破壞,只是被掩蓋了,因此這樣的操做後續可能會有很是大的隱患,我的不建議這樣使用,須要在迭代過程操做集合的仍是應該用迭代器的方法。
另外,其實除了ArrayList之外,會發現HashMap中也會有modCount屬性,而在其相應的結構性操做方法內部,如put()、clear()等都會有對modCount++
操做,而在HashMap內部也有一個內部迭代器HashIterator,內部會維護一個expectedModCount屬性,其他的套路就都和ArrayList相似了。