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

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

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

 

Java代碼  收藏代碼
  1. public static void main(String[] args) {  
  2.  List<String> list = new ArrayList<String>();  
  3.          list.add("1");  
  4.           list.add("2");  
  5.           list.add("3");  
  6.           list.add("4");  
  7.           list.add("5");  
  8.           list.add("6");  
  9.           list.add("7");  
  10.             
  11.  List<String> del = new ArrayList<String>();  
  12.           del.add("5");  
  13.           del.add("6");  
  14.           del.add("7");    
  15.   
  16. <span style="color: #ff0000;">for(String str : list){  
  17.      if(del.contains(str)) {  
  18.             list.remove(str);            
  19. }  
  20.           }</span>  
  21.  }  

 運行這段代碼會出現如下異常:

 

Java代碼  收藏代碼
  1. Exception in thread "main" java.util.ConcurrentModificationException  

 

 

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

 JDK java.util. AbstractList類中相關源碼

Java代碼  收藏代碼
  1. public Iterator<E> iterator() {  
  2.    return new Itr();  
  3. }  

 

 

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

 

Java代碼  收藏代碼
  1. private class Itr implements Iterator<E> {  
  2.     /** 
  3.      * Index of element to be returned by subsequent call to next. 
  4.      */  
  5.     int cursor = 0;  
  6.   
  7.     /** 
  8.      * Index of element returned by most recent call to next or 
  9.      * previous.  Reset to -1 if this element is deleted by a call 
  10.      * to remove. 
  11.      */  
  12.     int lastRet = -1;  
  13.   
  14.     /** 
  15.      * The modCount value that the iterator believes that the backing 
  16.      * List should have.  If this expectation is violated, the iterator 
  17.      * has detected concurrent modification. 
  18.      */  
  19.     int expectedModCount = modCount;  
  20.   
  21.     public boolean hasNext() {  
  22.             return cursor != size();  
  23.     }  
  24.   
  25.     public E next() {  
  26.             checkForComodification(); //檢測modCount和expectedModCount的值!!  
  27.         try {  
  28.         E next = get(cursor);  
  29.         lastRet = cursor++;  
  30.         return next;  
  31.         } catch (IndexOutOfBoundsException e) {  
  32.         checkForComodification();  
  33.         throw new NoSuchElementException();  
  34.         }  
  35.     }  
  36.   
  37.     public void remove() {  
  38.         if (lastRet == -1)  
  39.         throw new IllegalStateException();  
  40.             checkForComodification();  
  41.   
  42.         try {  
  43.         AbstractList.this.remove(lastRet); //執行remove的操作  
  44.         if (lastRet < cursor)  
  45.             cursor--;  
  46.         lastRet = -1;  
  47.         expectedModCount = modCount; //保證了modCount和expectedModCount的值的一致性,避免拋出ConcurrentModificationException異常  
  48.         } catch (IndexOutOfBoundsException e) {  
  49.         throw new ConcurrentModificationException();  
  50.         }  
  51.     }  
  52.   
  53.     final void checkForComodification() {  
  54.         if (modCount != expectedModCount) //當modCount和expectedModCount值不相等時,則拋出ConcurrentModificationException異常  
  55.         throw new ConcurrentModificationException();  
  56.     }  
  57.     }  

 

 

再看一下ArrayList 的 remove方法

 

Java代碼  收藏代碼
  1. public boolean remove(Object o) {  
  2.     if (o == null) {  
  3.             for (int index = 0; index < size; index++)  
  4.         if (elementData[index] == null) {  
  5.             fastRemove(index);  
  6.             return true;  
  7.         }  
  8.     } else {  
  9.         for (int index = 0; index < size; index++)  
  10.         if (o.equals(elementData[index])) {  
  11.             fastRemove(index);  
  12.             return true;  
  13.         }  
  14.         }  
  15.     return false;  
  16.     }  
  17.   
  18.     /* 
  19.      * Private remove method that skips bounds checking and does not 
  20.      * return the value removed. 
  21.      */  
  22.     private void fastRemove(int index) {  
  23.         modCount++; //只是修改了modCount,因此modCount將與expectedModCount的值不一致  
  24.         int numMoved = size - index - 1;  
  25.         if (numMoved > 0)  
  26.             System.arraycopy(elementData, index+1, elementData, index,  
  27.                              numMoved);  
  28.         elementData[--size] = null// Let gc do its work  
  29.     }   
 

 

回過頭去看看java.util. AbstractListnext()方法

 

Java代碼  收藏代碼
  1. public E next() {  
  2.             checkForComodification(); //檢測modCount和expectedModCount的值!!  
  3.         try {  
  4.         E next = get(cursor);  
  5.         lastRet = cursor++;  
  6.         return next;  
  7.         } catch (IndexOutOfBoundsException e) {  
  8.         checkForComodification();  
  9.         throw new NoSuchElementException();  
  10.         }  
  11.     }  
  12.    
  13. final void checkForComodification() {  
  14.         if (modCount != expectedModCount) //當modCount和expectedModCount值不相等時,則拋出ConcurrentModificationException異常  
  15.         throw new ConcurrentModificationException();  
  16.     }  
  17.     }  

 

 

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

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

 

 

解決辦法如下:

(1)  新建一個集合存放要刪除的對象,等遍歷完後,調用removeAll(Collection<?> c)方法

把上面例子中迭代集合的代碼替換成:

 

 

Java代碼  收藏代碼
  1. List<String> save = new ArrayList<String>();  
  2.             
  3.           for(String str : list)  
  4.           {  
  5.            if(del.contains(str))  
  6.            {  
  7.                save.add(str);  
  8.            }  
  9.           }  
  10.           list.removeAll(save);  
 

 

   (2) 使用Iterator替代增強型for循環:

 

Java代碼  收藏代碼
  1. Iterator<String> iterator = list.iterator();  
  2.      while(iterator.hasNext()) {  
  3.          String str = iterator.next();  
  4.          if(del.contains(str)) {  
  5.              iterator.remove();  
  6.          }  
  7.      }  

      Iterator.remove()方法保證了modCountexpectedModCount的值的一致性,避免拋出ConcurrentModificationException異常。

 

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

 

Java代碼  收藏代碼
  1. Iterator<String> iterator = list.iterator();    
  2. synchronized(synObject) {  
  3.  while(iterator.hasNext()) {    
  4.          String str = iterator.next();    
  5.          if(del.contains(str)) {    
  6.              iterator.remove();    
  7.          }    
  8.      }    
  9. }  
  10.       
 

因爲迭代器實現類如:ListItr的next(),previous(),remove(),set(E e),add(E e)這些方法都會調用checkForComodification(),源碼:

 

Java代碼  收藏代碼
  1. final void checkForComodification() {  
  2.         if (modCount != expectedModCount)  
  3.         throw new ConcurrentModificationException();  
  4.     }  
 

 

 

 

 

 

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

 

Java代碼  收藏代碼
  1. Iterator<Integer> iterator = windows.keySet().iterator();  
  2.         while(iterator.hasNext()) {  
  3.             int type = iterator.next();  
  4.             windows.get(type).closeWindow();  
  5.             iterator.remove();  
  6.             windows.remove(type);   //  
  7.         }  

 

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

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

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

 

 

Java代碼  收藏代碼
  1. public void remove() {  
  2.             if (lastEntryReturned == null)  
  3.                 throw new IllegalStateException();  
  4.             if (modCount != expectedModCount)  
  5.                 throw new ConcurrentModificationException();  
  6.             HashMap.this.remove(lastEntryReturned.key);  
  7.             lastEntryReturned = null;  
  8.             expectedModCount = modCount; //保證了這兩值的一致性  
  9.         }  

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

windows.remove(type)的源碼:

 

 

Java代碼  收藏代碼
  1. public V remove(Object key) {  
  2.         if (key == null) {  
  3.             return removeNullKey();  
  4.         }  
  5.         int hash = secondaryHash(key.hashCode());  
  6.         HashMapEntry<K, V>[] tab = table;  
  7.         int index = hash & (tab.length - 1);  
  8.         for (HashMapEntry<K, V> e = tab[index], prev = null;  
  9.                 e != null; prev = e, e = e.next) {  
  10.             if (e.hash == hash && key.equals(e.key)) {  
  11.                 if (prev == null) {  
  12.                     tab[index] = e.next;  
  13.                 } else {  
  14.                     prev.next = e.next;  
  15.                 }  
  16.                 modCount++;  
  17.                 size--;  
  18.                 postRemove(e);  
  19.                 return e.value;  
  20.             }  
  21.         }  
  22.         return null;  
  23.     }  

 上面的代碼中,由於先調用了iterator.remove();所以再調用HashMap的remove方法時,key就已經爲null了,所以會執行:removeNullKey();

方法,removeNullKey()源碼:

 

 

Java代碼  收藏代碼
  1. private V removeNullKey() {  
  2.        HashMapEntry<K, V> e = entryForNullKey;  
  3.        if (e == null) {  
  4.            return null;  
  5.        }  
  6.        entryForNullKey = null;  
  7.        modCount++;  
  8.        size--;  
  9.        postRemove(e);  
  10.        return e.value;  
  11.    }  

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

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

 

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

Iterator 是工作在一個獨立的線程中,並且擁有一個 mutex 鎖。 Iterator 被創建之後會建立一個指向原來對象的單鏈索引表,當原來的對象數量發生變化時,這個索引表的內容不會同步改變,所以當索引指針往後移動的時候就找不到要迭代的對象,所以按照 fail-fast 原則 Iterator 會馬上拋出 java.util.ConcurrentModificationException 異常。

所以 Iterator 在工作的時候是不允許被迭代的對象被改變的。但你可以使用 Iterator 本身的方法 remove() 來刪除對象, Iterator.remove() 方法會在刪除當前迭代對象的同時維護索引的一致性。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章