List集合循環remove/add時報錯ConcurrentModificationException

在這裏插入圖片描述
其實關於這個問題,在阿里巴巴Java開發手冊中有規定:
在這裏插入圖片描述
WHY ?

查看源碼之後才知道是因爲
在這裏插入圖片描述

那麼modCount和expectedModCount是什麼呢?
在這裏插入圖片描述

modCount是ArrayList中的一個成員變量。它表示該集合實際被修改的次數。
expectedModCount 是 ArrayList中的一個內部類——Itr中的成員變量。

expectedModCount表示這個迭代器預期該集合被修改的次數。其值隨着Itr被創建而初始化。只有通過迭代器對集合進行操作,該值纔會改變。

那麼,接着我們看下userNames.remove(userName);方法裏面做了什麼事情,爲什麼會導致expectedModCount和modCount的值不一樣。

通過翻閱代碼,我們也可以發現,remove方法核心邏輯如下:
在這裏插入圖片描述
可以看到,remove方法只修改了modCount,並沒有對expectedModCount做任何操作。

之所以會拋出CMException異常,是因爲我們的代碼中使用了增強for循環,而在增強for循環中,集合遍歷是通過iterator進行的,但是元素的add/remove卻是直接使用的集合類自己的方法。這就導致iterator在遍歷的時候,會發現有一個元素在自己不知不覺的情況下就被刪除/添加了,就會拋出一個異常,用來提示用戶,可能發生了併發修改!
所以,在使用Java的集合類的時候,如果發生CMException,優先考慮fail-fast有關的情況,實際上這裏並沒有真的發生併發,只是Iterator使用了fail-fast的保護機制,只要他發現有某一次修改是未經過自己進行的,那麼就會拋出異常

使用CopyOnWriteArrayList代替了ArrayList,就不會發生異常。

fail-safe集合的所有對集合的修改都是先拷貝一份副本,然後在副本集合上進行的,並不是直接對原集合進行修改。並且這些修改方法,如add/remove都是通過加鎖來控制併發的。

所以,CopyOnWriteArrayList中的迭代器在迭代的過程中不需要做fail-fast的併發檢測。(因爲fail-fast的主要目的就是識別併發,然後通過異常的方式通知用戶)

但是,雖然基於拷貝內容的優點是避免了ConcurrentModificationException,但同樣地,迭代器並不能訪問到修改後的內容。如以下代碼:

public static void main(String[] args) {
    List<String> userNames = new CopyOnWriteArrayList<String>() {{
        add("Hollis");
        add("hollis");
        add("HollisChuang");
        add("H");
    }};

    Iterator it = userNames.iterator();

    for (String userName : userNames) {
        if (userName.equals("Hollis")) {
            userNames.remove(userName);
        }
    }

    System.out.println(userNames);

    while(it.hasNext()){
        System.out.println(it.next());
    }
}

我們得到CopyOnWriteArrayList的Iterator之後,通過for循環直接刪除原數組中的值,最後在結尾處輸出Iterator,結果發現內容如下:

[hollis, HollisChuang, H]
Hollis
hollis
HollisChuang
H

迭代器遍歷的是開始遍歷那一刻拿到的集合拷貝,在遍歷期間原集合發生的修改迭代器是不知道的。
其他方式

  • 使用迭代器(Iterator)+while循環,
  • 普通fori循環,
  • 使用Java 8中提供的filter過濾(重新生成一個集合)
  • 使用增強for循環其實也可以(找到這個元素刪除後 break推出循環)
  • 使用fail-safe的集合類(ConcurrentLinkedDeque)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章