【傳智播客.黑馬程序員訓練營成都中心】
- 轉載請注明出處
作者:成都校區(qū).堂堂老師
1. 什么時候會產生并發(fā)修改異常
并發(fā)的意思是同時發(fā)生,那么其實并發(fā)修改的字面意思就是同時修改铡溪,通過查看JDK的API我們可以得知,并發(fā)修改異常的出現(xiàn)的原因是:當方法檢測到對象的并發(fā)修改,但不允許這種修改時伙菊,拋出此異常抖韩。
-
一個常見的場景就是:當我們在對集合進行迭代操作的時候蛀恩,如果同時對集合對象中的元素進行某些操作,則容易導致并發(fā)修改異常的產生茂浮。
- 例如我們要完成以下需求:
- 在一個存儲字符串的集合中双谆,如果存在字符串"Java",則添加一個"Android"
- 示范代碼如下:
public class Test { public static void main(String[] args){ ArrayList<String> list = new ArrayList<String>(); list.add("Java"); list.add("Hello"); list.add("World"); Iterator<String> it = list.iterator();//獲取迭代器對象 while(it.hasNext()){ //如果迭代器判斷集合中還有下一個元素則繼續(xù)循環(huán) String str = it.next();//獲取集合中迭代器所指向的元素 if(str.equals("Java")) {//如果這個元素內容是"Java" list.add("Android");//則在集合中添加一個"Android" } } } }
- 控制臺輸出:
Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859) at java.util.ArrayList$Itr.next(ArrayList.java:831) at com.itheima.day02.Test5.main(Test5.java:17)
控制臺顯示的ConcurrentModificationException席揽,即并發(fā)修改異常
下面我們就以ArrayList集合中出現(xiàn)的并發(fā)修改異常為例來分析異常產生的原因顽馋。
2. 異常是如何產生的
-
2.1 想要知道異常出現(xiàn)的原因,我們需要找到源碼中異常出現(xiàn)的根源
-
我們能通過控制臺找到異常的根源:
- at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859)
- 異常出現(xiàn)的位置出現(xiàn)在ArrayList類中內部類Itr中的checkForComodification方法
-
貼出此方法的源碼:
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
由此方法可知幌羞,當一個名為modCount的變量值不等于expectedModCount的變量值時寸谜,異常對象被拋出。
-
-
2.2 繼續(xù)探究這兩個變量分別是代表什么
-
modCount
modCount是定義在AbstractList抽象類中的public修飾的成員變量新翎,而ArrayList是此類的子類程帕,那么代表ArrayList繼承到了modCount這個變量。
-
源碼中對modCount的解釋是:
The number of times this list has been <i>structurally modified</i>
- 我們可以理解為:這個變量其實就代表了集合在結構上修改的次數(shù)
- expectedModCount
- expectedModCount是內部類Itr中的成員變量地啰,當ArrayList對象調用iteroter()方法時愁拭,會創(chuàng)建內部類Itr的對象,并給其成員變量expectedModCount賦值為ArrayList對象成員變量的值modCount亏吝。以下是內部類Itr的部分源碼
private class Itr implements Iterator<E> { int cursor; int lastRet = -1; int expectedModCount = modCount; ....
- 由此可知岭埠,當Itr對象被創(chuàng)建的時候,expectedModCount的值會等于modCount變量的值。
- 由此可知岭埠,當Itr對象被創(chuàng)建的時候,expectedModCount的值會等于modCount變量的值。
-
那么modCount變量在賦值給expectedModCount之前又會如何變化呢惜论?
當我們創(chuàng)建ArrayList對象的時候许赃,ArrayList對象里包含了此變量modCount并且初始化值為0;
-
通過查看源碼,我們能發(fā)現(xiàn)在ArrayList類中有操作modCount的方法都是添加元素的相關功能和刪除元素的相關功能馆类。例如:
- 每刪除一個元素混聊,modCount的值會自增一次
public E remove(int index) { rangeCheck(index); modCount++; ...//此處省略代碼 E oldValue = elementData(index); return oldValue; }
- 在add方法中會調用下面的方法,意味著每添加一個元素乾巧,modCount的值也會自增一次
private void ensureExplicitCapacity(int minCapacity) { modCount++; if (minCapacity - elementData.length > 0) grow(minCapacity); }
-
也就是說:我們每次進行對集合中的元素個數(shù)變化的操作時句喜,modCount的值就會+1
- 但是這個操作僅限于增刪元素,修改元素值并不會影響modCount的值
-
再結合API中對此變量的解釋沟于,我們可以得出大致的判斷:
-
其實modCount變量就是記錄了對集合元素個數(shù)的改變次數(shù)
-
其實modCount變量就是記錄了對集合元素個數(shù)的改變次數(shù)
-
modCount
-
2.3 分析完這兩個關鍵的變量咳胃,我們再結合迭代器的工作流程來分析異常出現(xiàn)的過程
-
2.3.1 迭代器的創(chuàng)建
上文中已經提到過,當ArrayList對象調用iteroter()方法時旷太,會創(chuàng)建內部類Itr的對象展懈。
-
此時迭代器對象中有兩個最關鍵的成員變量:cursor、expectedModCount
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; .....//此處省略下方其他源碼 }
-
cursor
- 迭代器的工作就是將集合中的元素逐個取出供璧,而cursor就是迭代器中用于指向集合中某個元素的指針
- 在迭代器迭代的過程中存崖,cursor初始值為0,每次取出一個元素嗜傅,cursor值會+1金句,以便下一次能指向下一個元素,直到cursor值等于集合的長度為止吕嘀,從而達到取出所有元素的效果违寞。
-
expectedModCount
- expectedModCount在迭代器對象創(chuàng)建時被賦值為modCount
- 上文已經分析過,modCount應該理解為集合元素個數(shù)的改變次數(shù)偶房,或者說結構修改次數(shù)
- 也就是說趁曼,當創(chuàng)建完迭代器對象后,如果我們沒有對集合結構進行修改棕洋,expectedModCount的值是會等于modCount的值的挡闰。
- 在迭代集合元素的過程中,迭代器通過檢查expectedModCount和modCount的值是否相同掰盘,以防止出現(xiàn)并發(fā)修改摄悯。
-
2.3.2 迭代器迭代過程源碼分析:
- 在2.3.1中我們已經簡要的分析過了迭代器工作中最重要的兩個變量,下面貼出更多源碼結合上文的分析繼續(xù)說明迭代器是如何工作的愧捕。
-
我們在使用迭代器的時候奢驯,一般會調用迭代器的hasNext()方法判斷是否還有下一個元素,此方法源碼非常簡單:
public boolean hasNext() { return cursor != size; }
分析:
- cursor初始值是0次绘,默認指向集合中第一個元素瘪阁,每次取出一個元素撒遣,cursor值就會自增一次
- size是集合中的成員變量,用于表示集合的元素個數(shù)
- 因為集合中最后一個元素的索引為size-1,只要cursor值不等于size那么就證明還有下一個元素管跺,此時hasNext方法返回true义黎,如若cursor值與size相等了,那么證明已經迭代完了最后一個元素豁跑,此方法返回false廉涕。
- 當我們通過迭代器的hasNext方法返回true值確信集合中還有元素的時候,通常我們會通過迭代器的另一個方法next取出此元素艇拍。源碼如下:public E next() { checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1; return (E) elementData[lastRet = i]; } //在next方法的第一行調用了此方法 final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
分析:
- next()方法第一行就是調用checkForComodification()方法火的,也就是我們上文中分析過并發(fā)修改異常出現(xiàn)根源
- 當?shù)魍ㄟ^next()方法返回元素之前都會檢查集合中的modCount和最初賦值給迭代器的expectedModCount是否相等,如果不等淑倾,則拋出并發(fā)修改異常。
- 也就說征椒,當?shù)鞴ぷ鞯倪^程中娇哆,不允許集合擅自修改集合結構,如果修改了會導致modCount值變化勃救,從而不會等于expectedModCount碍讨,那么迭代器就會拋出并發(fā)修改異常。
- 如果沒有異常產生蒙秒,next()方法最后一行會返回cursor指向的元素勃黍。
-
- 在2.3.1中我們已經簡要的分析過了迭代器工作中最重要的兩個變量,下面貼出更多源碼結合上文的分析繼續(xù)說明迭代器是如何工作的愧捕。
-
2.3.1 迭代器的創(chuàng)建
3. 并發(fā)修改異常的作用及解決方案
-
3.1 在上文中我們已經結合源碼仔細的分析了并發(fā)修改異常產生的原因以及過程,那么這個異常的產生對程序而言究竟有什么意義呢晕讲?
- 我們通過上文的分析其實可以知道覆获,迭代器是通過cursor指針指向對應集合元素來挨個獲取集合中元素的,每次獲取對應元素后cursor值+1指向下一個元素瓢省,直到集合最后一個元素弄息。
- 那么如果在迭代器獲取元素的過程中,集合中元素的個數(shù)突然改變勤婚,那么下一次獲取元素時摹量,cursor能否正確的指向集合的下一個元素就變得未知了,這種不確定性有可能導致迭代器工作出現(xiàn)意想不到的問題馒胆。
- 為了防止在將來某個時間任意發(fā)生不確定行為的風險缨称,我們在使用迭代器的過程中不允許修改集合結構(也可以說是不允許修改元素個數(shù)),否則迭代器會拋出異常結束程序祝迂。
-
3.2 那如果如果遇到需要在遍歷集合的同時修改集合結構的需求如何處理睦尽?
- 3.2.1 在迭代器迭代的過程中,我們雖然不能通過集合直接增刪元素液兽,但是其實迭代器中是有這樣的方法可以實現(xiàn)增刪的骂删。
-
通過ArrayList中iterator()方法返回的Itr迭代器對象包含有一個remove方法:
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(); } }
-
除了通過iterator()方法返回的Itr迭代器對象之外掌动,我們可以獲取Itr迭代器的子類對象ListItr,ListItr中有添加元素的add方法:
public void add(E e) { checkForComodification(); try { int i = cursor; ArrayList.this.add(i, e); cursor = i + 1; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } }
-
- 以上兩個方法在增刪完元素后都對指針cursor進行了相應的處理宁玫,避免了出現(xiàn)迭代器獲取元素的不確定行為粗恢。
- 3.2.2 異常是迭代器拋出的,那么我們除了可以使用迭代器遍歷集合欧瘪,還可以使用其他方法眷射,比如:
-
屬于List體系的集合我們可以使用用普通for循環(huán),通過索引獲取集合元素的方法來遍歷集合佛掖,這個時候修改集合結構是不會出現(xiàn)異常的妖碉。
public static void main(String[] args){ ArrayList<String> list = new ArrayList<String>(); list.add("Java"); list.add("Hello"); list.add("World"); for (int i = 0; i < list.size(); i++) { String element = list.get(i); if(element.equals("Java")){ /* 注意: * 當集合中增刪元素后 i 索引的指向元素有可能發(fā)生變化, * 我們通常會在增刪元素的同時讓i變量也隨之變化芥被, * 從而使 i 能正確指向下一個元素:list.remove(i--); */ list.remove(i); } } }
那么不屬于List體系的集合欧宜,我們也可通過單列集合頂層接口Collction中定義過的toArray方法將集合轉為數(shù)組,這個時候就不需要擔心出現(xiàn)并發(fā)修改異常了拴魄。
-
- 3.2.1 在迭代器迭代的過程中,我們雖然不能通過集合直接增刪元素液兽,但是其實迭代器中是有這樣的方法可以實現(xiàn)增刪的骂删。
4. 其他相關問題
-
4.1 foreach循環(huán)和迭代器
foreach循環(huán)也就是我們常說的增強for循環(huán)冗茸,其實foreach循環(huán)的底層是用迭代器實現(xiàn)的
-
我們可以通過斷點調試操作如下范例代碼證明上面的觀點:
public static void main(String[] args){ ArrayList<String> list = new ArrayList<String>(); list.add("Java"); list.add("Hello"); list.add("World"); for (String s : list) { System.out.println(s);//在此行代碼打上斷點,然后開啟debug運行程序 } }
- 在輸出語句這一行打上斷點匹中,當程序執(zhí)行到輸出語句這一行時夏漱,eclipse跳入debug視圖
- 接著按下F6結束這一步,debug上顯示執(zhí)行for循環(huán)上的代碼顶捷,此時按下F5進入代碼挂绰,會發(fā)現(xiàn)程序的執(zhí)行來到了ArrayList類中內部類Itr中的hasNext()方法中。
- 由此可見服赎,foreach循環(huán)底層是用迭代器來實現(xiàn)的葵蒂。
-
既然foreach底層是用迭代器實現(xiàn)的,那么就意味著:
-
我們不能在foreach中對集合結構進行修改专肪。否則有可能出現(xiàn)并發(fā)修改異常
-
我們不能在foreach中對集合結構進行修改专肪。否則有可能出現(xiàn)并發(fā)修改異常
-
4.2 當?shù)良系箶?shù)第二個元素的同時刹勃,刪除集合元素不會導致并發(fā)修改異常
-
這是一個很有意思的問題,我們先來一段范例代碼:
public static void main(String[] args){ ArrayList<String> list = new ArrayList<String>(); list.add("Java"); list.add("Hello"); list.add("World"); for (String s : list) { if(s.equals("Hello")){ list.remove("Java"); } } System.out.println(list);//控制臺輸出:[Hello, World] }
- 上面的代碼在foreach中當?shù)恋教幍诙€元素"Hello"的時候嚎尤,我們刪除了元素"Java"荔仁,但是并沒有出現(xiàn)并發(fā)修改異常,控制臺輸出了剩余的兩個元素也證明這次刪除確實成功了芽死。
- 如果不是迭代至倒數(shù)第二個元素時刪除元素同樣會導致異常的產生乏梁,這又是為什么呢?
-
原因解釋:
- 集合中倒數(shù)第二個元素的索引為size - 2关贵,當?shù)魅〕黾系箶?shù)第二個元素的時候遇骑,cursor指向的位置會向右移動一位,值會變?yōu)閟ize - 1揖曾;
- 如果此時通過集合去刪除一個元素落萎,集合中元素個數(shù)會減一亥啦,所以size值會變?yōu)閟ize - 1;
- 當?shù)髟噲D去獲取最后一個元素的時候练链,會先判斷是否還有元素翔脱,調用hasNext()方法,上文中已經分析過媒鼓,hasNext()方法會返回cursor!=size届吁,但是此時的cursor和此時的size值都等于刪除之前的size - 1,兩者相等绿鸣,那么hasNext()方法就會返回false疚沐,迭代器就不會再調用next方法獲取元素了。
-