java.util.ConcurrentModificationException 異常問題詳解

環境:JDK 1.8.0_111html

在Java開發過程當中,使用iterator遍歷集合的同時對集合進行修改就會出現java.util.ConcurrentModificationException異常,本文就以ArrayList爲例去理解和解決這種異常。java

1、單線程狀況下問題分析及解決方案

1.1 問題復現

先上一段拋異常的代碼。android

複製代碼
複製代碼
1     public void test1()  {
 2         ArrayList<Integer> arrayList = new ArrayList<>();
 3         for (int i = 0; i < 20; i++) {
 4             arrayList.add(Integer.valueOf(i));
 5         }
 6 
 7         // 復現方法一
 8         Iterator<Integer> iterator = arrayList.iterator();
 9         while (iterator.hasNext()) {
10             Integer integer = iterator.next();
11             if (integer.intValue() == 5) {
12                 arrayList.remove(integer);
13             }
14         }
15 
16         // 復現方法二
17         iterator = arrayList.iterator();
18         for (Integer value : arrayList) {
19             Integer integer = iterator.next();
20             if (integer.intValue() == 5) {
21                 arrayList.remove(integer);
22             }
23         }
24     }
複製代碼
複製代碼

在這個代碼中展現了兩種能拋異常的實現方式。程序員

1.二、問題緣由分析

先來看實現方法一,方法一中使用Iterator遍歷ArrayList, 拋出異常的是iterator.next()。看下Iterator next方法實現源碼數組

複製代碼
複製代碼
1         public E next() {
 2             checkForComodification();
 3             int i = cursor;
 4             if (i >= size)
 5                 throw new NoSuchElementException();
 6             Object[] elementData = ArrayList.this.elementData;
 7             if (i >= elementData.length)
 8                 throw new ConcurrentModificationException();
 9             cursor = i + 1;
10             return (E) elementData[lastRet = i];
11         }
12 
13         final void checkForComodification() {
14             if (modCount != expectedModCount)
15                 throw new ConcurrentModificationException();
16         }
複製代碼
複製代碼

在next方法中首先調用了checkForComodification方法,該方法會判斷modCount是否等於expectedModCount,不等於就會拋出java.util.ConcurrentModificationExcepiton異常。安全

咱們接下來跟蹤看一下modCount和expectedModCount的賦值和修改。多線程

modCount是ArrayList的一個屬性,繼承自抽象類AbstractList,用於表示ArrayList對象被修改次數。ide

1 protected transient int modCount = 0;

整個ArrayList中修改modCount的方法比較多,有add、remove、clear、ensureCapacityInternal等,凡是設計到ArrayList對象修改的都會自增modCount屬性。測試

在建立Iterator的時候會將modCount賦值給expectedModCount,在遍歷ArrayList過程當中,沒有其餘地方能夠設置expectedModCount了,所以遍歷過程當中expectedModCount會一直保持初始值20(調用add方法添加了20個元素,修改了20次)。字體

1 int expectedModCount = modCount; // 建立對象時初始化

遍歷的時候是不會觸發modCount自增的,可是遍歷到integer.intValue() == 5的時候,執行了一次arrayList.remove(integer),這行代碼執行後modCount++變爲了21,但此時的expectedModCount仍然爲20。

1         final void checkForComodification() {
2             if (modCount != expectedModCount)
3                 throw new ConcurrentModificationException();
4         }

在執行next方法時,遇到modCount != expectedModCount方法,致使拋出異常java.util.ConcurrentModificationException。

明白了拋出異常的過程,可是爲何要這麼作呢?很明顯這麼作是爲了阻止程序員在不容許修改的時候修改對象,起到保護做用,避免出現未知異常。引用網上的一段解釋,點擊查看解釋來源

Iterator 是工做在一個獨立的線程中,而且擁有一個 mutex 鎖。 
Iterator 被建立以後會創建一個指向原來對象的單鏈索引表,當原來的對象數量發生變化時,這個索引表的內容不會同步改變。
當索引指針日後移動的時候就找不到要迭代的對象,因此按照 fail-fast 原則 Iterator 會立刻拋出 java.util.ConcurrentModificationException 異常。
因此 Iterator 在工做的時候是不容許被迭代的對象被改變的。但你可使用 Iterator 自己的方法 remove() 來刪除對象, Iterator.remove() 方法會在刪除當前迭代對象的同時維護索引的一致性。

再來分析下第二種for循環拋異常的緣由:

複製代碼
複製代碼
1     public void forEach(Consumer<? super E> action) {
 2         Objects.requireNonNull(action);
 3         final int expectedModCount = modCount;
 4         @SuppressWarnings("unchecked")
 5         final E[] elementData = (E[]) this.elementData;
 6         final int size = this.size;
 7         for (int i=0; modCount == expectedModCount && i < size; i++) {
 8             action.accept(elementData[i]);
 9         }
10         if (modCount != expectedModCount) {
11             throw new ConcurrentModificationException();
12         }
13     }
複製代碼
複製代碼

在for循環中一開始也是對expectedModCount採用modCount進行賦值。在進行for循環時每次都會有斷定條件modCount == expectedModCount,當執行完arrayList.remove(integer)以後,該斷定條件返回false退出循環,而後執行if語句,結果一樣拋出java.util.ConcurrentModificationException異常。

這兩種復現方法實際上都是同一個緣由致使的。

1.3 問題解決方案

上述的兩種復現方法都是在單線程運行的,先來講明單線程中的解決方案:

複製代碼
複製代碼
1     public void test2() {
 2         ArrayList<Integer> arrayList = new ArrayList<>();
 3         for (int i = 0; i < 20; i++) {
 4             arrayList.add(Integer.valueOf(i));
 5         }
 6 
 7         Iterator<Integer> iterator = arrayList.iterator();
 8         while (iterator.hasNext()) {
 9             Integer integer = iterator.next();
10             if (integer.intValue() == 5) {
11                 iterator.remove();
12             }
13         }
14     }
複製代碼
複製代碼

這種解決方案最核心的就是調用iterator.remove()方法。咱們看看該方法源碼爲何這個方法能避免拋出異常

複製代碼
複製代碼
1         public void remove() {
 2             if (lastRet < 0)
 3                 throw new IllegalStateException();
 4             checkForComodification();
 5 
 6             try {
 7                 ArrayList.this.remove(lastRet);
 8                 cursor = lastRet;
 9                 lastRet = -1;
10                 expectedModCount = modCount;
11             } catch (IndexOutOfBoundsException ex) {
12                 throw new ConcurrentModificationException();
13             }
14         }
複製代碼
複製代碼

在iterator.remove()方法中,一樣調用了ArrayList自身的remove方法,可是調用完以後並不是就return了,而是expectedModCount = modCount重置了expectedModCount值,使兩者的值繼續保持相等。

針對forEach循環並無修復方案,所以在遍歷過程當中同時須要修改ArrayList對象,則須要採用iterator遍歷。

上面提出的解決方案調用的是iterator.remove()方法,若是不只僅是想調用remove方法移除元素,還想增長元素,或者替換元素,是否能夠呢?瀏覽Iterator源碼能夠發現這是不行的,Iterator只提供了remove方法。

可是ArrayList實現了ListIterator接口,ListIterator類繼承了Iter,這些操做都是能夠實現的,使用示例以下:

複製代碼
複製代碼
1     public void test3() {
 2         ArrayList<Integer> arrayList = new ArrayList<>();
 3         for (int i = 0; i < 20; i++) {
 4             arrayList.add(Integer.valueOf(i));
 5         }
 6 
 7         ListIterator<Integer> iterator = arrayList.listIterator();
 8         while (iterator.hasNext()) {
 9             Integer integer = iterator.next();
10             if (integer.intValue() == 5) {
11                 iterator.set(Integer.valueOf(6));
12                 iterator.remove();
13                 iterator.add(integer);
14             }
15         }
16     }
複製代碼
複製代碼

2、 多線程狀況下的問題分析及解決方案

單線程問題解決了,再來看看多線程狀況。

2.1 問題復現

複製代碼
複製代碼
1     public void test4() {
 2         ArrayList<Integer> arrayList = new ArrayList<>();
 3         for (int i = 0; i < 20; i++) {
 4             arrayList.add(Integer.valueOf(i));
 5         }
 6 
 7         Thread thread1 = new Thread(new Runnable() {
 8             @Override
 9             public void run() {
10                 ListIterator<Integer> iterator = arrayList.listIterator();
11                 while (iterator.hasNext()) {
12                     System.out.println("thread1 " + iterator.next().intValue());
13                     try {
14                         Thread.sleep(1000);
15                     } catch (InterruptedException e) {
16                         e.printStackTrace();
17                     }
18                 }
19             }
20         });
21 
22         Thread thread2 = new Thread(new Runnable() {
23             @Override
24             public void run() {
25                 ListIterator<Integer> iterator = arrayList.listIterator();
26                 while (iterator.hasNext()) {
27                     System.out.println("thread2 " + iterator.next().intValue());
28                     iterator.remove();
29                 }
30             }
31         });
32         thread1.start();
33         thread2.start();
34     }
複製代碼
複製代碼

在個測試代碼中,開啓兩個線程,一個線程遍歷,另一個線程遍歷加修改。程序輸出結果以下

複製代碼
複製代碼
thread1 0
thread2 0
thread2 1
thread2 2
thread2 3
thread2 4
thread2 5
thread2 6
thread2 7
thread2 8
thread2 9
thread2 10
thread2 11
thread2 12
thread2 13
thread2 14
thread2 15
thread2 16
thread2 17
thread2 18
thread2 19
Exception in thread "Thread-0" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
	at java.util.ArrayList$Itr.next(ArrayList.java:851)
	at com.snow.ExceptionTest$1.run(ExceptionTest.java:74)
	at java.lang.Thread.run(Thread.java:745)

Process finished with exit code 0
複製代碼
複製代碼

2.2 問題分析

從上面代碼執行結果能夠看出thread2 遍歷結束後,thread1 sleep完1000ms準備遍歷第二個元素,next的時候拋出異常了。咱們從時間點分析一下拋異常的緣由

時間點 arrayList.modCount thread1 iterator.expectedModCount thread2 iterator.expectedModCount
thread start,初始化iterator 20 20 20
thread2.remove()調用以後 21 20 21

 

 

 

兩個thread都是使用的同一個arrayList,thread2修改完後modCount = 21,此時thread2的expectedModCount = 21 能夠一直遍歷到結束;thread1的expectedModCount仍然爲20,由於thread1的expectedModCount只是在初始化的時候賦值,其後並未被修改過。所以當arrayList的modCount被thread2修改成21以後,thread1想繼續遍歷一定會拋出異常了。

在這個示例代碼裏面,兩個thread,每一個thread都有本身的iterator,當thread2經過iterator方法修改expectedModCount一定不會被thread1感知到。這個跟ArrayList非線程安全是無關的,即便這裏面的ArrayList換成Vector也是同樣的結果,不信上測試代碼:

複製代碼
複製代碼
1     public void test5() {
 2         Vector<Integer> vector = new Vector<>();
 3         for (int i = 0; i < 20; i++) {
 4             vector.add(Integer.valueOf(i));
 5         }
 6 
 7         Thread thread1 = new Thread(new Runnable() {
 8             @Override
 9             public void run() {
10                 ListIterator<Integer> iterator = vector.listIterator();
11                 while (iterator.hasNext()) {
12                     System.out.println("thread1 " + iterator.next().intValue());
13                     try {
14                         Thread.sleep(1000);
15                     } catch (InterruptedException e) {
16                         e.printStackTrace();
17                     }
18                 }
19             }
20         });
21 
22         Thread thread2 = new Thread(new Runnable() {
23             @Override
24             public void run() {
25                 ListIterator<Integer> iterator = vector.listIterator();
26                 while (iterator.hasNext()) {
27                     Integer integer = iterator.next();
28                     System.out.println("thread2 " + integer.intValue());
29                     if (integer.intValue() == 5) {
30                         iterator.remove();
31                     }
32                 }
33             }
34         });
35         thread1.start();
36         thread2.start();
37     }
複製代碼
複製代碼

執行後輸出結果爲:

複製代碼
複製代碼
thread1 0
thread2 0
thread2 1
thread2 2
thread2 3
thread2 4
thread2 5
thread2 6
thread2 7
thread2 8
thread2 9
thread2 10
thread2 11
thread2 12
thread2 13
thread2 14
thread2 15
thread2 16
thread2 17
thread2 18
thread2 19
Exception in thread "Thread-0" java.util.ConcurrentModificationException
	at java.util.Vector$Itr.checkForComodification(Vector.java:1184)
	at java.util.Vector$Itr.next(Vector.java:1137)
	at com.snow.ExceptionTest$3.run(ExceptionTest.java:112)
	at java.lang.Thread.run(Thread.java:745)

Process finished with exit code 0
複製代碼
複製代碼

test5()方法執行結果和test4()是相同的,那如何解決這個問題呢?

2.3 多線程下的解決方案

2.3.1 方案一:iterator遍歷過程加同步鎖,鎖住整個arrayList

複製代碼
複製代碼
1     public static void test5() {
 2         ArrayList<Integer> arrayList = new ArrayList<>();
 3         for (int i = 0; i < 20; i++) {
 4             arrayList.add(Integer.valueOf(i));
 5         }
 6 
 7         Thread thread1 = new Thread(new Runnable() {
 8             @Override
 9             public void run() {
10                 synchronized (arrayList) {
11                     ListIterator<Integer> iterator = arrayList.listIterator();
12                     while (iterator.hasNext()) {
13                         System.out.println("thread1 " + iterator.next().intValue());
14                         try {
15                             Thread.sleep(100);
16                         } catch (InterruptedException e) {
17                             e.printStackTrace();
18                         }
19                     }
20                 }
21             }
22         });
23 
24         Thread thread2 = new Thread(new Runnable() {
25             @Override
26             public void run() {
27                 synchronized (arrayList) {
28                     ListIterator<Integer> iterator = arrayList.listIterator();
29                     while (iterator.hasNext()) {
30                         Integer integer = iterator.next();
31                         System.out.println("thread2 " + integer.intValue());
32                         if (integer.intValue() == 5) {
33                             iterator.remove();
34                         }
35                     }
36                 }
37             }
38         });
39         thread1.start();
40         thread2.start();
41     }
複製代碼
複製代碼

這種方案本質上是將多線程經過加鎖來轉變爲單線程操做,確保同一時間內只有一個線程去使用iterator遍歷arrayList,其它線程等待,效率顯然是隻有單線程的效率。

2.3.2 方案二:使用CopyOnWriteArrayList,有坑!要明白原理再用,不然你就呆坑裏吧。

咱們先來看代碼,頗有意思咯

複製代碼
複製代碼
1     public void test6() {
 2         List<Integer> list = new CopyOnWriteArrayList<>();
 3         for (int i = 0; i < 20; i++) {
 4             list.add(Integer.valueOf(i));
 5         }
 6 
 7         Thread thread1 = new Thread(new Runnable() {
 8             @Override
 9             public void run() {
10                 ListIterator<Integer> iterator = list.listIterator();
11                 while (iterator.hasNext()) {
12                     System.out.println("thread1 " + iterator.next().intValue());
13                     try {
14                         Thread.sleep(1000);
15                     } catch (InterruptedException e) {
16                         e.printStackTrace();
17                     }
18                 }
19             }
20         });
21 
22         Thread thread2 = new Thread(new Runnable() {
23             @Override
24             public void run() {
25                 for (Integer integer : list) {
26                     System.out.println("thread2 " + integer.intValue());
27                     if (integer.intValue() == 5) {
28                         list.remove(integer);
29                     }
30                 }
31                 for (Integer integer : list) {
32                     System.out.println("thread2 again " + integer.intValue());
33                 }
34 //                ListIterator<Integer> iterator = list.listIterator();
35 //                while (iterator.hasNext()) {
36 //                    Integer integer = iterator.next();
37 //                    System.out.println("thread2 " + integer.intValue());
38 //                    if (integer.intValue() == 5) {
39 //                        iterator.remove();
40 //                    }
41 //                }
42             }
43         });
44         thread1.start();
45         thread2.start();
46     }
複製代碼
複製代碼

先不分析,看執行結果,這個執行結果重點關注字體加粗部分。

複製代碼
複製代碼
thread1 0
thread2 0
thread2 1
thread2 2
thread2 3
thread2 4
thread2 5
thread2 6
thread2 7
thread2 8
thread2 9
thread2 10
thread2 11
thread2 12
thread2 13
thread2 14
thread2 15
thread2 16
thread2 17
thread2 18
thread2 19
thread2 again 0
thread2 again 1
thread2 again 2
thread2 again 3
thread2 again 4
thread2 again 6
thread2 again 7
thread2 again 8
thread2 again 9
thread2 again 10
thread2 again 11
thread2 again 12
thread2 again 13
thread2 again 14
thread2 again 15
thread2 again 16
thread2 again 17
thread2 again 18
thread2 again 19
thread1 1
thread1 2
thread1 3
thread1 4
thread1 5
thread1 6
thread1 7
thread1 8
thread1 9
thread1 10
thread1 11
thread1 12
thread1 13
thread1 14
thread1 15
thread1 16
thread1 17
thread1 18
thread1 19

Process finished with exit code 0
複製代碼
複製代碼

咱們先分析thread2的輸出結果,第一次遍歷將4 5 6都輸出,情理之中;第一次遍歷後刪除掉了一個元素,第二次遍歷輸出4 6,符合咱們的預期。

再來看下thread1的輸出結果,有意思的事情來了,thread1 仍然輸出了4 5 6,什麼鬼?thread1和thread2都是遍歷list,list在thread1遍歷第二個元素的時候就已經刪除了一個元素了,爲啥還能輸出5?

爲了瞭解這個問題,須要瞭解CopyOnWriteArrayList是如何作到一邊遍歷的同時還能一邊修改而且還不拋異常的。

在這裏不想再深刻分析CopyOnWriteArrayList代碼,後續會專門出一篇博客來解釋這個類的源碼的。

這裏說一下CopyOnWriteArrayList的解決思路,其實很簡單:

1 private transient volatile Object[] array;

CopyOnWriteArrayList本質上是對array數組的一個封裝,一旦CopyOnWriteArrayList對象發生任何的修改都會new一個新的Object[]數組newElement,在newElement數組上執行修改操做,修改完成後將newElement賦值給array數組(array=newElement)。

由於array是volatile的,所以它的修改對全部線程均可見。

瞭解了CopyOnWriteArrayList的實現思路以後,咱們再來分析上面代碼test6爲何會出現那樣的輸出結果。先來看下thread1和thread2中用到的兩種遍歷方式的源碼:

複製代碼
複製代碼
1     public void forEach(Consumer<? super E> action) {
 2         if (action == null) throw new NullPointerException();
 3         // 在遍歷開始前獲取當前數組
 4         Object[] elements = getArray();
 5         int len = elements.length;
 6         for (int i = 0; i < len; ++i) {
 7             @SuppressWarnings("unchecked") E e = (E) elements[i];
 8             action.accept(e);
 9         }
10     }
複製代碼
複製代碼

 

複製代碼
複製代碼
1 public ListIterator<E> listIterator() {
 2         return new COWIterator<E>(getArray(), 0);
 3     }
 4     static final class COWIterator<E> implements ListIterator<E> {
 5         /** Snapshot of the array */
 6         private final Object[] snapshot;
 7         /** Index of element to be returned by subsequent call to next.  */
 8         private int cursor;
 9 
10         private COWIterator(Object[] elements, int initialCursor) {
11             cursor = initialCursor;
12             // 初始化爲當前數組
13             snapshot = elements;
14         }
15 
16         public void remove() {
17             // 已經不支持Iterator remove操做了!!
18             throw new UnsupportedOperationException();
19         }
20 
21         public boolean hasNext() {
22             return cursor < snapshot.length;
23         }
24 
25         @SuppressWarnings("unchecked")
26         public E next() {
27             if (! hasNext())
28                 throw new NoSuchElementException();
29             return (E) snapshot[cursor++];
30         }
31 
32         // 此處省略其餘無關代碼
33     }
複製代碼
複製代碼

這兩種遍歷方式有個共同的特色:都在初始化的時候將當前數組保存下來了,以後的遍歷都將會遍歷這個數組,而無論array如何變化。

時間點 CopyOnWriteArrayList的array thread1 iterator 初始化的Object數組 thread2 第一次遍歷forEach初始化的Object數組 thread2 第二次遍歷forEach初始化的Object數組
thread start 假設爲A A A /
thread2 調用remove方法以後 假設爲B A A B

 

 

 

有了這個時間節點表就很清楚了,thread1和thread2 start的時候都會將A數組初始化給本身的臨時變量,以後遍歷的也都是這個A數組,而無論CopyOnWriteArrayList中的array發生了什麼變化。所以也就解釋了thread1在thread2 remove掉一個元素以後爲何還會輸出5了。在thread2中,第二次遍歷初始化數組變成了當前的array,也就是修改後的B,所以不會有Integer.valueOf(5)這個元素了。

從test6執行結果來看,CopyOnWriteArrayList確實能解決一邊遍歷一邊修改而且還不會拋異常,可是這也是有代價的:

(1) thread2對array數組的修改thread1並不能被動感知到,只能經過hashCode()方法去主動感知,不然就會一直使用修改前的數據

(2) 每次修改都須要從新new一個數組,而且將array數組數據拷貝到new出來的數組中,效率會大幅降低

此外CopyOnWriteArrayList中的ListIterator實現是不支持remove、add和set操做的,一旦調用就會拋出UnsupportedOperationException異常,所以test6註釋代碼34-41行中若是運行是會拋異常的。

參考文獻: 

http://lz12366.iteye.com/blog/675016 

http://www.cnblogs.com/dolphin0520/p/3933551.html

http://blog.csdn.net/androiddevelop/article/details/21509345

梅花香自古寒來
相關文章
相關標籤/搜索