Java并发修改异常的源码解析
1. 什么時候會產生并發修改異常
- 并發的意思是同時發生,那么其實并發修改的字面意思就是同時修改,通過查看JDK的API我們可以得知,并發修改異常的出現的原因是:當方法檢測到對象的并發修改,但不允許這種修改時,拋出此異常。
-
一個常見的場景就是:當我們在對集合進行迭代操作的時候,如果同時對集合對象中的元素進行某些操作,則容易導致并發修改異常的產生。
- 例如我們要完成以下需求:?
- 在一個存儲字符串的集合中,如果存在字符串”Java”,則添加一個”Android”
- 示范代碼如下:
- ?
- 控制臺輸出:
- ?
- 例如我們要完成以下需求:?
- 控制臺顯示的ConcurrentModificationException,即并發修改異常
- 下面我們就以ArrayList集合中出現的并發修改異常為例來分析異常產生的原因。
2. 異常是如何產生的
-
2.1 想要知道異常出現的原因,我們需要找到源碼中異常出現的根源
- 我們能通過控制臺找到異常的根源:?
- at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859)
- 異常出現的位置出現在ArrayList類中內部類Itr中的checkForComodification方法
-
貼出此方法的源碼:
final void checkForComodification() {if (modCount != expectedModCount)throw new ConcurrentModificationException(); }- ?
- 由此方法可知,當一個名為modCount的變量值不等于expectedModCount的變量值時,異常對象被拋出。
?
- 我們能通過控制臺找到異常的根源:?
-
2.2 繼續探究這兩個變量分別是代表什么
-
modCount
- modCount是定義在AbstractList抽象類中的public修飾的成員變量,而ArrayList是此類的子類,那么代表ArrayList繼承到了modCount這個變量。
-
源碼中對modCount的解釋是:
The number of times this list has been?structurally modified?
- 我們可以理解為:這個變量其實就代表了集合在結構上修改的次數
?
- 我們可以理解為:這個變量其實就代表了集合在結構上修改的次數
-
expectedModCount
-
expectedModCount是內部類Itr中的成員變量,當ArrayList對象調用iteroter()方法時,會創建內部類Itr的對象,并給其成員變量expectedModCount賦值為ArrayList對象成員變量的值modCount。
以下是內部類Itr的部分源碼,我們主要看此內部類成員變量部分
private class Itr implements Iterator<E> {int cursor; int lastRet = -1; int expectedModCount = modCount;.... }- ?
- 由此段代碼可知,當Itr對象被創建的時候,expectedModCount的值會等于modCount變量的值。
?
-
-
那么modCount變量在賦值給expectedModCount之前又會如何變化呢?
- 當我們創建ArrayList對象的時候,ArrayList對象里包含了此變量modCount并且初始化值為0;
-
通過查看源碼,我們能發現在ArrayList類中有操作modCount的方法都是添加元素的相關功能和刪除元素的相關功能。例如:
- 每刪除一個元素,modCount的值會自增一次
- ?
- 在add方法中會調用下面的方法,意味著每添加一個元素,modCount的值也會自增一次
- ?
- 也就是說:我們每次進行對集合中的元素個數變化的操作時,modCount的值就會+1?
- 但是這個操作僅限于增刪元素,修改元素值并不會影響modCount的值
- 再結合API中對此變量的解釋,我們可以得出大致的判斷:?
- 其實modCount變量就是記錄了對集合元素個數的改變次數
?
- 其實modCount變量就是記錄了對集合元素個數的改變次數
-
-
2.3 分析完這兩個關鍵的變量,我們再結合迭代器的工作流程來分析異常出現的過程
-
2.3.1 迭代器的創建
- 上文中已經提到過,當ArrayList對象調用iteroter()方法時,會創建內部類Itr的對象。
-
此時迭代器對象中有兩個最關鍵的成員變量:cursor、expectedModCount
private class Itr implements Iterator<E> {int cursor; // index of next element to returnint lastRet = -1; // index of last element returned; -1 if no suchint expectedModCount = modCount;.....//此處省略下方其他源碼 }- ?
- cursor?
- 迭代器的工作就是將集合中的元素逐個取出,而cursor就是迭代器中用于指向集合中某個元素的指針
- 在迭代器迭代的過程中,cursor初始值為0,每次取出一個元素,cursor值會+1,以便下一次能指向下一個元素,直到cursor值等于集合的長度為止,從而達到取出所有元素的效果。
- expectedModCount?
- expectedModCount在迭代器對象創建時被賦值為modCount
- 上文已經分析過,modCount應該理解為集合元素個數的改變次數,或者說結構修改次數
- 也就是說,當創建完迭代器對象后,如果我們沒有對集合結構進行修改,expectedModCount的值是會等于modCount的值的。
- 在迭代集合元素的過程中,迭代器通過檢查expectedModCount和modCount的值是否相同,以防止出現并發修改。
?
-
2.3.2 迭代器迭代過程源碼分析:
-
在2.3.1中我們已經簡要的分析過了迭代器工作中最重要的兩個變量,下面貼出更多源碼結合上文的分析繼續說明迭代器是如何工作的。
-
我們在使用迭代器的時候,一般會調用迭代器的hasNext()方法判斷是否還有下一個元素,此方法源碼非常簡單:
public boolean hasNext() {return cursor != size; }- ?
分析:
- cursor初始值是0,默認指向集合中第一個元素,每次取出一個元素,cursor值就會自增一次
- size是集合中的成員變量,用于表示集合的元素個數
- 因為集合中最后一個元素的索引為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()方法,也就是我們上文中分析過并發修改異常出現根源
- 當迭代器通過next()方法返回元素之前都會檢查集合中的modCount和最初賦值給迭代器的expectedModCount是否相等,如果不等,則拋出并發修改異常。
- 也就說,當迭代器工作的過程中,不允許集合擅自修改集合結構,如果修改了會導致modCount值變化,從而不會等于expectedModCount,那么迭代器就會拋出并發修改異常。
- 如果沒有異常產生,next()方法最后一行會返回cursor指向的元素。
-
-
-
3. 并發修改異常的意義及異常解決方案
- 3.1?在上文中我們已經結合源碼仔細的分析了并發修改異常產生的原因以及過程,那么這個異常的產生對程序而言究竟有什么意義呢??
- 我們通過上文的分析其實可以知道,迭代器是通過cursor指針指向對應集合元素來挨個獲取集合中元素的,每次獲取對應元素后cursor值+1指向下一個元素,直到集合最后一個元素。
- 那么如果在迭代器獲取元素的過程中,集合中元素的個數突然改變,那么下一次獲取元素時,cursor能否正確的指向集合的下一個元素就變得未知了,這種不確定性有可能導致迭代器工作出現意想不到的問題。
- 為了防止在將來某個時間任意發生不確定行為的風險,我們在使用迭代器的過程中不允許修改集合結構(也可以說是不允許修改元素個數),否則迭代器會拋出異常結束程序。
?
-
3.2?那如果如果遇到需要在遍歷集合的同時修改集合結構的需求如何處理?
-
3.2.1 在迭代器迭代的過程中,我們雖然不能通過集合直接增刪元素,但是其實迭代器中是有這樣的方法可以實現增刪的。
-
通過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進行了相應的處理,避免了出現迭代器獲取元素的不確定行為。
? -
3.2.2 異常是迭代器拋出的,那么我們除了可以使用迭代器遍歷集合,還可以使用其他方法,比如:
-
屬于List體系的集合我們可以使用用普通for循環,通過索引獲取集合元素的方法來遍歷集合,這個時候修改集合結構是不會出現異常的。
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 索引的指向元素有可能發生變化,* 我們通常會在增刪元素的同時讓i變量也隨之變化,* 從而使 i 能正確指向下一個元素:list.remove(i--);*/list.remove(i);}} }- ?
- 那么不屬于List體系的集合,我們也可通過單列集合頂層接口Collction中定義過的toArray方法將集合轉為數組,這個時候就不需要擔心出現并發修改異常了。
-
-
4. 其他相關問題
-
4.1?foreach循環和迭代器
- foreach循環也就是我們常說的增強for循環,其實foreach循環的底層是用迭代器實現的
-
我們可以通過斷點調試操作如下范例代碼證明上面的觀點:
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運行程序} }- ?
- 在輸出語句這一行打上斷點,當程序執行到輸出語句這一行時,eclipse跳入debug視圖
- 接著按下F6結束這一步,debug上顯示執行for循環上的代碼,此時按下F5進入代碼,會發現程序的執行來到了ArrayList類中內部類Itr中的hasNext()方法中。?
- 由此可見,foreach循環底層是用迭代器來實現的。
- 既然foreach底層是用迭代器實現的,那么就意味著:?
- 我們不能在foreach中對集合結構進行修改。否則有可能出現并發修改異常
?
- 我們不能在foreach中對集合結構進行修改。否則有可能出現并發修改異常
-
4.2?當迭代至集合倒數第二個元素的同時,刪除集合元素不會導致并發修改異常
-
這是一個很有意思的問題,我們先來一段范例代碼:
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中當迭代至到處第二個元素”Hello”的時候,我們刪除了元素”Java”,但是并沒有出現并發修改異常,控制臺輸出了剩余的兩個元素也證明這次刪除確實成功了。
- 如果不是迭代至倒數第二個元素時刪除元素同樣會導致異常的產生,這又是為什么呢?
- 原因解釋:?
- 集合中倒數第二個元素的索引為size - 2,當迭代器取出集合倒數第二個元素的時候,cursor指向的位置會向右移動一位,值會變為size - 1;
- 如果此時通過集合去刪除一個元素,集合中元素個數會減一,所以size值會變為size - 1;
- 當迭代器試圖去獲取最后一個元素的時候,會先判斷是否還有元素,調用hasNext()方法,上文中已經分析過,hasNext()方法會返回cursor!=size,但是此時的cursor和此時的size值都等于刪除之前的size - 1,兩者相等,那么hasNext()方法就會返回false,迭代器就不會再調用next方法獲取元素了。
-
總結
以上是生活随笔為你收集整理的Java并发修改异常的源码解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: STM32开发 -- 蓝牙开发详解(2)
- 下一篇: STM32开发 -- 可调直流稳压电源