一、單線程
1. 異常情況舉例
隻要抛出出現異常,可以肯定的是代碼一定有錯誤的地方。先來看看都有哪些情況會出現ConcurrentModificationException異常,下面以ArrayList remove 操作進行舉例:
使用的資料集合:
?
1 2 3 4 5 6 7 | |
以下三種情況都會出現異常: ?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | |
異常資訊如下:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.AbstractList$Itr.checkForComodification(Unknown Source)
at java.util.AbstractList$Itr.next(Unknown Source)
2. 根本原因
以上都有3種出現異常的情況有一個共同的特點,都是使用Iterator進行周遊,且都是通過ArrayList.remove(Object) 進行删除操作。
想要找出根本原因,直接檢視ArrayList 源碼看為什麼出現異常:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | |
Iterator是工作在一個獨立的線程中,并且擁有一個mutex鎖,就是說Iterator在工作的時候,是不允許被疊代的對象被改變的。Iterator被建立的時候,建立了一個記憶體索引表(單連結清單),這個索引表指向原來的對象,當原來的對象數量改變的時候,這個索引表的内容沒有同步改變,是以當索引指針往下移動的時候,便找不到要疊代的對象,于是産生錯誤。List、Set等是動态的,可變對象數量的資料結構,但是Iterator則是單向不可變,隻能順序讀取,不能逆序操作的資料結構,當Iterator指向的原始資料發生變化時,Iterator自己就迷失了方向。
List、Set、Map 都可以通過Iterator進行周遊,這裡僅僅是通過List舉例,在使用其他集合周遊時進行增删操作都需要留意是否會觸發ConcurrentModificationException異常。
3. 解決方案
上面列舉了會出現問題的幾種情況,也分析了問題出現的根本原因,現在來總結一下怎樣才是正确的,如果避免周遊時進行增删操作不會出現ConcurrentModificationException異常。
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | |
輸出結果都是:List Value:[1, 2, 4, 5] , 不會出現異常。
以上4種解決辦法在單線程中測試完全沒有問題,但是如果在多線程中呢?
二、多線程
1. 同步異常情況舉例
上面針對ConcurrentModificationException異常在單線程情況下提出了4種解決方案,本來是可以很哈皮的洗洗睡了,但是如果涉及到多線程環境可能就不那麼樂觀了。
下面的例子中開啟兩個子線程,一個進行周遊,另外一個有條件删除元素:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | |
輸出結果:
周遊集合 value = 1
删除元素 value = 1
周遊集合 value = 2
删除元素 value = 2
周遊集合 value = 3
删除元素 value = 3
Exception in thread "Thread-0" 删除元素 value = 4
java.util.ConcurrentModificationException
at java.util.AbstractList$Itr.checkForComodification(Unknown Source)
at java.util.AbstractList$Itr.next(Unknown Source)
at list.ConcurrentModificationExceptionStudy$1.run(ConcurrentModificationExceptionStudy.java:42)
at java.lang.Thread.run(Unknown Source)
删除元素 value = 5
結論:
上面的例子在多線程情況下,僅使用單線程周遊中進行删除的第1種解決方案使用it.remove(),但是測試得知4種的解決辦法中的1、2、3依然會出現問題。
接着來再看一下 JavaDoc對java.util.ConcurrentModificationException異常的描述:
當方法檢測到對象的并發修改,但不允許這種修改時,抛出此異常。
說明以上辦法在同一個線程執行的時候是沒問題的,但是在異步情況下依然可能出現異常。
2. 嘗試方案
(1) 在所有周遊增删地方都加上synchronized或者使用Collections.synchronizedList,雖然能解決問題但是并不推薦,因為增删造成的同步鎖可能會阻塞周遊操作。
(2) 推薦使用ConcurrentHashMap或者CopyOnWriteArrayList。
3. CopyOnWriteArrayList注意事項
(1) CopyOnWriteArrayList不能使用Iterator.remove()進行删除。
(2) CopyOnWriteArrayList使用Iterator且使用List.remove(Object);會出現如下異常:
java.lang.UnsupportedOperationException: Unsupported operation remove
at java.util.concurrent.CopyOnWriteArrayList$ListIteratorImpl.remove(CopyOnWriteArrayList.java:804)
4. 解決方案
單線程情況下列出4種解決方案,但是發現在多線程情況下僅有第4種方案才能在多線程情況下不出現問題。
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | |
輸出結果:
删除元素 value = 1
周遊集合 value = 1
删除元素 value = 2
周遊集合 value = 2
删除元素 value = 3
周遊集合 value = 3
删除元素 value = 4
周遊集合 value = 4
删除元素 value = 5
周遊集合 value = 5
OK,搞定
三、參考資料
《How to Avoid ConcurrentModificationException when using an Iterator》