發現問題
筆者最近在除錯專案bug的時候,遇到了一個很奇怪的bug,就是在對hashmap集合進行遍歷的時候,同時做了remove操作,這個操作最後導致丟擲了java.util.ConcurrentModificationException的錯誤。
帶著疑惑,下面參考著原始碼,分析問題的原因。
首先,重現問題,構造一個map並往裡面加元素:
private static HashMap<Integer, String> map = new HashMap<Integer, String>();;
public static void main(String[] args) {
for(int i = 0; i < 10; i++){
map.put(i, "value" + i);
}
}
複製程式碼
然後移除一些元素,此時就會報java.util.ConcurrentModificationException錯誤
for(Map.Entry<Integer, String> entry : map.entrySet()){
Integer key = entry.getKey();
if(key % 2 == 0){
System.out.println("To delete key " + key);
map.remove(key);
System.out.println("The key " + + key + " was deleted");
}
複製程式碼
分析問題
從報錯中可以看出,HashMap$HashIterator.nextNode這個方法有程式碼錯誤了,點進去看,大概知道HashMap.this.modCount != this.expectedModCount 成立
再看一下hashmap的remove操作是做了什麼:
這裡對modCount進行了自增操作,表示操作動作+1。再看modCount和expectedModCount是什麼東西
問題原因
可以看出迭代器初始化的時候就對modCount和expectedModCount進行同步。
到此,可以看出報錯的原因:
- hashmap裡維護了一個modCount變數,迭代器裡維護了一個expectedModCount變數,一開始兩者是一樣的。
- 每次進行hashmap.remove操作的時候就會對modCount+1,此時迭代器裡的expectedModCount還是之前的值。
- 在下一次對迭代器進行next()呼叫時,判斷是否HashMap.this.modCount != this.expectedModCount,如果是則丟擲異常。
解決問題
那什麼情況下在遍歷的時候可以刪除map裡面的元素呢?看下迭代器提供的remove方法:
可以看出迭代器裡remove了一個元素之後會對expectedModCount重新賦值,這樣再次遍歷的時候就不會報錯了。所以之前的程式碼可以改成如下寫法,直接呼叫迭代器的remove方法。
Iterator<Map.Entry<Integer, String>> it = map.entrySet().iterator();
while(it.hasNext()){
Map.Entry<Integer, String> entry = it.next();
Integer key = entry.getKey();
if(key % 2 == 0){
System.out.println("To delete key " + key);
it.remove();
System.out.println("The key " + + key + " was deleted");
}
}
複製程式碼
總結
- 基本上java集合類(包括list和map)在遍歷時沒用迭代器進行刪除了都會報ConcurrentModificationException錯誤,這是一種fast-fail的機制,初衷是為了檢測bug。
- 通俗一點來說,這種機制就是為了防止高併發的情況下,多個執行緒同時修改map或者list的元素導致的資料不一致,這是隻要判斷當前modCount != expectedModCount即可以知道有其他執行緒修改了集合。
替換機制:
- 用迭代器的remove方法。
- 用currentHashMap替代HashMap