其實關於這個問題,在阿里巴巴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)