Java ConcurrentModificationException 異常分析與解決方案

\

 

 

1、單線程

1. 異常狀況舉例

只要拋出出現異常,能夠確定的是代碼必定有錯誤的地方。先來看看都有哪些狀況會出現ConcurrentModificationException異常,下面以ArrayList remove 操做進行舉例:

使用的數據集合:html

?java

1安全

2多線程

3併發

4dom

5異步

6ide

7測試

List<string> myList = new ArrayList<string>();this

 

myList.add( "1");

myList.add( "2");

myList.add( "3");

myList.add( "4");

myList.add( "5");</string></string>


如下三種狀況都會出現異常:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

Iterator<string> it = myList.iterator();

 while (it.hasNext()) {

     String value = it.next();

      if (value.equals( "3")) {

          myList.remove(value);  // error

     }

}

 

 for (Iterator<string> it = myList.iterator(); it.hasNext();) {

     String value = it.next();

      if (value.equals( "3")) {

          myList.remove(value);  // error

     }

}

 

 

 for (String value : myList) {

     System. out.println( "List Value:" + value);

      if (value.equals( "3")) {

          myList.remove(value);  // error

     }

}   </string></string>


異常信息以下:
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

public class ArrayList<e> extends AbstractList<e>

        implements Cloneable, Serializable, RandomAccess {

         

         

         @Override public boolean remove(Object object) {

        Object[] a = array;

        int s = size;

        if (object != null) {

            for (int i = 0; i < s; i++) {

                if (object.equals(a[i])) {

                    System.arraycopy(a, i + 1, a, i, --s - i);

                    a[s] = null// Prevent memory leak

                    size = s;

                    modCount++;  // 只要刪除成功都是累加

                    return true;

                }

            }

        } else {

            for (int i = 0; i < s; i++) {

                if (a[i] == null) {

                    System.arraycopy(a, i + 1, a, i, --s - i);

                    a[s] = null// Prevent memory leak

                    size = s;

                    modCount++;  // 只要刪除成功都是累加

                    return true;

                }

            }

        }

        return false;

    }  

 

 

    @Override public Iterator<e> iterator() {

        return new ArrayListIterator();

    }  

         

    private class ArrayListIterator implements Iterator<e> {

          ......

    

          // 全局修改總數保存到當前類中

        /** The expected modCount value */

        private int expectedModCount = modCount;

 

        @SuppressWarnings("unchecked") public E next() {

            ArrayList<e> ourList = ArrayList.this;

            int rem = remaining;

               // 若是建立時的值不相同,拋出異常

            if (ourList.modCount != expectedModCount) {

                throw new ConcurrentModificationException();

            }

            if (rem == 0) {

                throw new NoSuchElementException();

            }

            remaining = rem - 1;

            return (E) ourList.array[removalIndex = ourList.size - rem];

        }  

         

          ......

     }

}   </e></e></e></e></e>


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

// 1 使用Iterator提供的remove方法,用於刪除當前元素

 for (Iterator<string> it = myList.iterator(); it.hasNext();) {

     String value = it.next();

      if (value.equals( "3")) {

          it.remove();  // ok

     }

}

System. out.println( "List Value:" + myList.toString());

 

 // 2 建一個集合,記錄須要刪除的元素,以後統一刪除            

List<string> templist = new ArrayList<string>();

 for (String value : myList) {

      if (value.equals( "3")) {

          templist.remove(value);

     }

}

 // 能夠查看removeAll源碼,其中使用Iterator進行遍歷

myList.removeAll(templist);

System. out.println( "List Value:" + myList.toString());       

 

  // 3. 使用線程安全CopyOnWriteArrayList進行刪除操做

List<string> myList = new CopyOnWriteArrayList<string>();

myList.add( "1");

myList.add( "2");

myList.add( "3");

myList.add( "4");

myList.add( "5");

 

Iterator<string> it = myList.iterator();

 

 while (it.hasNext()) {

     String value = it.next();

      if (value.equals( "3")) {

          myList.remove( "4");

          myList.add( "6");

          myList.add( "7");

     }

}

System. out.println( "List Value:" + myList.toString());

 

 // 4. 不使用Iterator進行遍歷,須要注意的是本身保證索引正常

 for ( int i = 0; i < myList.size(); i++) {

     String value = myList.get(i);

     System. out.println( "List Value:" + value);

      if (value.equals( "3")) {

          myList.remove(value);  // ok

          i--; // 由於位置發生改變,因此必須修改i的位置

     }

}

System. out.println( "List Value:" + myList.toString());</string></string></string></string></string></string>


輸出結果都是:List Value:[1, 2, 4, 5] , 不會出現異常。
以上4種解決辦法在單線程中測試徹底沒有問題,可是若是在多線程中呢?

 

2、多線程

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

final List<string> myList = createTestData();

 

new Thread(new Runnable() {

  

     @Override

     public void run() {

          for (String string : myList) {

               System.out.println("遍歷集合 value = " + string);

            

               try {

                    Thread.sleep(100);

               } catch (InterruptedException e) {

                    e.printStackTrace();

               }

          }

     }

}).start();

 

new Thread(new Runnable() {

  

     @Override

     public void run() {

       

       for (Iterator<string> it = myList.iterator(); it.hasNext();) {

           String value = it.next();

        

           System.out.println("刪除元素 value = " + value);

        

           if (value.equals( "3")) {

                it.remove();

           }

        

           try {

                    Thread.sleep(100);

               } catch (InterruptedException e) {

                    e.printStackTrace();

               }

      }

     }

}).start();</string></string>


輸出結果: 
遍歷集合 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種的解決辦法中的一、二、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

List<string> myList = new CopyOnWriteArrayList<string>();

 myList.add( "1");

 myList.add( "2");

 myList.add( "3");

 myList.add( "4");

 myList.add( "5");

 

new Thread(new Runnable() {

   

     @Override

     public void run() {

          for (String string : myList) {

               System.out.println("遍歷集合 value = " + string);

             

               try {

                    Thread.sleep(100);

               } catch (InterruptedException e) {

                    e.printStackTrace();

               }

          }

     }

}).start();

 

new Thread(new Runnable() {

   

     @Override

     public void run() {

        

          for (int i = 0; i < myList.size(); i++) {

               String value = myList.get(i);

             

               System.out.println("刪除元素 value = " + value);

         

           if (value.equals( "3")) {

                myList.remove(value);

                i--; // 注意                          

           }

         

           try {

                    Thread.sleep(100);

               } catch (InterruptedException e) {

                    e.printStackTrace();

               }

          }

     }

}).start();</string></string>



輸出結果:
刪除元素 value = 1
遍歷集合 value = 1
刪除元素 value = 2
遍歷集合 value = 2
刪除元素 value = 3
遍歷集合 value = 3
刪除元素 value = 4
遍歷集合 value = 4
刪除元素 value = 5
遍歷集合 value = 5

OK,搞定


 

3、參考資料

《How to Avoid ConcurrentModificationException when using an Iterator》 

相關文章
相關標籤/搜索