1. 程式人生 > >java.util.ConcurrentModificationException 出現的原因和解決辦法

java.util.ConcurrentModificationException 出現的原因和解決辦法

用iterator遍歷集合時碰到java.util.ConcurrentModificationException這個異常,

下面以List為例來解釋為什麼會報java.util.ConcurrentModificationException這個異常,程式碼如下:

public static void main(String[] args) {  
 List<String> list = new ArrayList<String>();  
         list.add("1");  
          list.add("2");  
          list
.add("3"); list.add("4"); list.add("5"); list.add("6"); list.add("7"); List<String> del = new ArrayList<String>(); del.add("5"); del.add("6"); del.add("7"); for(String str : list){ if(del.contains(str)) { list
.remove(str); } } }

執行這段程式碼會出現如下異常:

Exception in thread "main" java.util.ConcurrentModificationException  

for(String str : list) 這句話實際上是用到了集合的iterator() 方法

JDK java.util. AbstractList類中相關原始碼

public Iterator<E> iterator() {  
   return new Itr();  
}  

java.util. AbstractList的內部類Itr的原始碼如下:

private class Itr implements Iterator<E> {  
    /** 
     * Index of element to be returned by subsequent call to next. 
     */  
    int cursor = 0;  

    /** 
     * Index of element returned by most recent call to next or 
     * previous.  Reset to -1 if this element is deleted by a call 
     * to remove. 
     */  
    int lastRet = -1;  

    /** 
     * The modCount value that the iterator believes that the backing 
     * List should have.  If this expectation is violated, the iterator 
     * has detected concurrent modification. 
     */  
    int expectedModCount = modCount;  

    public boolean hasNext() {  
            return cursor != size();  
    }  

    public E next() {  
            checkForComodification(); //檢測modCount和expectedModCount的值!!  
        try {  
        E next = get(cursor);  
        lastRet = cursor++;  
        return next;  
        } catch (IndexOutOfBoundsException e) {  
        checkForComodification();  
        throw new NoSuchElementException();  
        }  
    }  

    public void remove() {  
        if (lastRet == -1)  
        throw new IllegalStateException();  
            checkForComodification();  

        try {  
        AbstractList.this.remove(lastRet); //執行remove的操作  
        if (lastRet < cursor)  
            cursor--;  
        lastRet = -1;  
        expectedModCount = modCount; //保證了modCount和expectedModCount的值的一致性,避免丟擲ConcurrentModificationException異常  
        } catch (IndexOutOfBoundsException e) {  
        throw new ConcurrentModificationException();  
        }  
    }  

    final void checkForComodification() {  
        if (modCount != expectedModCount) //當modCount和expectedModCount值不相等時,則丟擲ConcurrentModificationException異常  
        throw new ConcurrentModificationException();  
    }  
    }  

再看一下ArrayList 的 remove方法

public boolean remove(Object o) {  
    if (o == null) {  
            for (int index = 0; index < size; index++)  
        if (elementData[index] == null) {  
            fastRemove(index);  
            return true;  
        }  
    } else {  
        for (int index = 0; index < size; index++)  
        if (o.equals(elementData[index])) {  
            fastRemove(index);  
            return true;  
        }  
        }  
    return false;  
    }  

    /* 
     * Private remove method that skips bounds checking and does not 
     * return the value removed. 
     */  
    private void fastRemove(int index) {  
        modCount++; //只是修改了modCount,因此modCount將與expectedModCount的值不一致  
        int numMoved = size - index - 1;  
        if (numMoved > 0)  
            System.arraycopy(elementData, index+1, elementData, index,  
                             numMoved);  
        elementData[--size] = null; // Let gc do its work  
    }   

回過頭去看看java.util. AbstractList的next()方法

public E next() {  
            checkForComodification(); //檢測modCount和expectedModCount的值!!  
        try {  
        E next = get(cursor);  
        lastRet = cursor++;  
        return next;  
        } catch (IndexOutOfBoundsException e) {  
        checkForComodification();  
        throw new NoSuchElementException();  
        }  
    }  

final void checkForComodification() {  
        if (modCount != expectedModCount) //當modCount和expectedModCount值不相等時,則丟擲ConcurrentModificationException異常  
        throw new ConcurrentModificationException();  
    }  
    }  

現在真相終於大白了,ArrayList的remove方法只是修改了modCount的值,並沒有修改expectedModCount,導致modCount和expectedModCount的值的不一致性,當next()時則丟擲ConcurrentModificationException異常

因此使用Iterator遍歷集合時,不要改動被迭代的物件,可以使用 Iterator 本身的方法 remove() 來刪除物件, Iterator.remove() 方法會在刪除當前迭代物件的同時維護modCount和expectedModCount值的一致性。

解決辦法如下:

(1) 新建一個集合存放要刪除的物件,等遍歷完後,呼叫removeAll(Collection

List<String> save = new ArrayList<String>();  

          for(String str : list)  
          {  
           if(del.contains(str))  
           {  
               save.add(str);  
           }  
          }  
          list.removeAll(save);  

(2) 使用Iterator替代增強型for迴圈:

Iterator<String> iterator = list.iterator();  
     while(iterator.hasNext()) {  
         String str = iterator.next();  
         if(del.contains(str)) {  
             iterator.remove();  
         }  
     }  
  Iterator.remove()方法保證了modCount和expectedModCount的值的一致性,避免丟擲ConcurrentModificationException異常。

不過對於在多執行緒環境下對集合類元素進行迭代修改操作,最好把程式碼放在一個同步程式碼塊內,這樣才能保證modCount和expectedModCount的值的一致性,類似如下:

Iterator<String> iterator = list.iterator();    
synchronized(synObject) {  
 while(iterator.hasNext()) {    
         String str = iterator.next();    
         if(del.contains(str)) {    
             iterator.remove();    
         }    
     }    
}  

因為迭代器實現類如:ListItr的next(),previous(),remove(),set(E e),add(E e)這些方法都會呼叫checkForComodification(),原始碼:

final void checkForComodification() {  
        if (modCount != expectedModCount)  
        throw new ConcurrentModificationException();  
    }  

曾經寫了下面這段對HashMap進行迭代刪除操作的錯誤的程式碼:

Iterator<Integer> iterator = windows.keySet().iterator();  
        while(iterator.hasNext()) {  
            int type = iterator.next();  
            windows.get(type).closeWindow();  
            iterator.remove();  
            windows.remove(type);   //  
        }  

上面的程式碼也會導致ConcurrentModificationException的發生。罪魁禍首是windows.remove(type);這一句。

根據上面的分析我們知道iterator.remove();會維護modCount和expectedModCount的值的一致性,而windows.remove(type);這句是不會的。其實這句是多餘的,上面的程式碼去掉這句就行了。

iterator.remove()的原始碼如下:HashIterator類的remove()方法

public void remove() {  
            if (lastEntryReturned == null)  
                throw new IllegalStateException();  
            if (modCount != expectedModCount)  
                throw new ConcurrentModificationException();  
            HashMap.this.remove(lastEntryReturned.key);  
            lastEntryReturned = null;  
            expectedModCount = modCount; //保證了這兩值的一致性  
        }  

HashMap.this.remove(lastEntryReturned.key);這句程式碼說明windows.remove(type);是多餘的,因為已經刪除了該key對應的value。

windows.remove(type)的原始碼:

public V remove(Object key) {  
        if (key == null) {  
            return removeNullKey();  
        }  
        int hash = secondaryHash(key.hashCode());  
        HashMapEntry<K, V>[] tab = table;  
        int index = hash & (tab.length - 1);  
        for (HashMapEntry<K, V> e = tab[index], prev = null;  
                e != null; prev = e, e = e.next) {  
            if (e.hash == hash && key.equals(e.key)) {  
                if (prev == null) {  
                    tab[index] = e.next;  
                } else {  
                    prev.next = e.next;  
                }  
                modCount++;  
                size--;  
                postRemove(e);  
                return e.value;  
            }  
        }  
        return null;  
    }  

上面的程式碼中,由於先呼叫了iterator.remove();所以再呼叫HashMap的remove方法時,key就已經為null了,所以會執行:removeNullKey();

方法,removeNullKey()原始碼:

private V removeNullKey() {  
       HashMapEntry<K, V> e = entryForNullKey;  
       if (e == null) {  
           return null;  
       }  
       entryForNullKey = null;  
       modCount++;  
       size--;  
       postRemove(e);  
       return e.value;  
   }  

不過不管是執行removeNullKey()還是key != null,如果直接呼叫HashMap的remove方法,都會導致ConcurrentModificationException

這個異常的發生,因為它對modCount++;沒有改變expectedModCount的值,沒有維護維護索引的一致性。

下面引用一段更專業的解釋:

Iterator 是工作在一個獨立的執行緒中,並且擁有一個 mutex 鎖。 Iterator 被建立之後會建立一個指向原來物件的單鏈索引表,當原來的物件數量發生變化時,這個索引表的內容不會同步改變,所以當索引指標往後移動的時候就找不到要迭代的物件,所以按照 fail-fast 原則 Iterator 會馬上丟擲 java.util.ConcurrentModificationException 異常。
所以 Iterator 在工作的時候是不允許被迭代的物件被改變的。但你可以使用 Iterator 本身的方法 remove() 來刪除物件, Iterator.remove() 方法會在刪除當前迭代物件的同時維護索引的一致性。