java.util.ConcurrentModificationException解決詳解

weixin_34391854發表於2017-10-25

異常產生

當我們迭代一個ArrayList或者HashMap時,如果嘗試對集合做一些修改操作(例如刪除元素),可能會丟擲java.util.ConcurrentModificationException的異常。

package reyo.sdk.utils.test.list2;

import java.util.ArrayList;
import java.util.List;

public class AddRemoveList {

	public static void main(String args[]) {
		List<String> list = new ArrayList<String>();
		list.add("A");
		list.add("B");

		for (String s : list) {
			if (s.equals("B")) {
				list.remove(s);
			}
		}

		//foreach迴圈等效於迭代器
		/*Iterator<String> iterator=list.iterator();
		while(iterator.hasNext()){
		    String s=iterator.next();
		    if (s.equals("B")) {
		        list.remove(s);
		    }
		}*/
	}
}

 出錯詳情:

 

異常原因

ArrayList的父類AbstarctList中有一個域modCount,每次對集合進行修改(增添元素,刪除元素……)時都會modCount++

而foreach的背後實現原理其實就是Iterator(關於Iterator可以看Java Design Pattern: Iterator),等同於註釋部分程式碼。在這裡,迭代ArrayList的Iterator中有一個變數expectedModCount,該變數會初始化和modCount相等,但如果接下來如果集合進行修改modCount改變,就會造成expectedModCount!=modCount,此時就會丟擲java.util.ConcurrentModificationException異常

過程如下圖:

我們再來根據原始碼詳細的走一遍這個過程

/*
 *AbstarctList的內部類,用於迭代
 */
private class Itr implements Iterator<E> {
    int cursor = 0;   //將要訪問的元素的索引
    int lastRet = -1;  //上一個訪問元素的索引
    int expectedModCount = modCount;//expectedModCount為預期修改值,初始化等於modCount(AbstractList類中的一個成員變數)

    //判斷是否還有下一個元素
    public boolean hasNext() {
            return cursor != size();
    }
    //取出下一個元素
    public E next() {
            checkForComodification();  //關鍵的一行程式碼,判斷expectedModCount和modCount是否相等
        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);
        if (lastRet < cursor)
            cursor--;
        lastRet = -1;
        expectedModCount = modCount;
        } catch (IndexOutOfBoundsException e) {
        throw new ConcurrentModificationException();
        }
    }

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

 

/* *AbstarctList的內部類,用於迭代 */ private class Itr implements Iterator<E> { int cursor = 0; //將要訪問的元素的索引 int lastRet = -1; //上一個訪問元素的索引 int expectedModCount = modCount;//expectedModCount為預期修改值,初始化等於modCount(AbstractList類中的一個成員變數) //判斷是否還有下一個元素 public boolean hasNext() { return cursor != size(); } //取出下一個元素 public E next() { checkForComodification(); //關鍵的一行程式碼,判斷expectedModCount和modCount是否相等 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); if (lastRet < cursor) cursor--; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException e) { throw new ConcurrentModificationException(); } } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }


作者:MrDTree
連結:http://www.jianshu.com/p/c5b52927a61a
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。
根據程式碼可知,每次迭代list時,會初始化Itr的三個成員變數
int cursor = 0;   //將要訪問的元素的索引
int lastRet = -1;  //上一個訪問元素的索引
int expectedModCount = modCount; //預期修改值,初始化等於modCount(AbstractList類中的一個成員變數)

 

接著呼叫hasNext()迴圈判斷訪問元素的下標是否到達末尾。如果沒有,呼叫next()方法,取出元素。
而最上面測試程式碼出現異常的原因在於,next()方法呼叫checkForComodification()時,發現了modCount != expectedModCount

接下來我們看下ArrayList的原始碼,瞭解下modCount 是如何與expectedModCount不相等的。

public boolean add(E paramE) {  
    ensureCapacityInternal(this.size + 1);  
    /** 省略此處程式碼 */  
}  

private void ensureCapacityInternal(int paramInt) {  
    if (this.elementData == EMPTY_ELEMENTDATA)  
        paramInt = Math.max(10, paramInt);  
    ensureExplicitCapacity(paramInt);  
}  

private void ensureExplicitCapacity(int paramInt) {  
    this.modCount += 1;    //修改modCount  
    /** 省略此處程式碼 */  
}  

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

private void fastRemove(int paramInt) {  
    this.modCount += 1;   //修改modCount  
    /** 省略此處程式碼 */  
}  

public void clear() {  
    this.modCount += 1;    //修改modCount  
    /** 省略此處程式碼 */  
}

 

從上面的程式碼可以看出,ArrayList的add、remove、clear方法都會造成modCount的改變。迭代過程中如何呼叫這些方法就會造成modCount的增加,使迭代類中expectedModCount和modCount不相等。

異常的解決

1. 單執行緒環境

好,現在我們已經基本瞭解了異常的傳送原因了。接下來我們來解決它。
我很任性,我就是想在迭代集合時刪除集合的元素,怎麼辦?

Iterator<String> iter = list.iterator();
while(iter.hasNext()){
    String str = iter.next();
      if( str.equals("B") )
      {
        iter.remove();
      }
}

 

細心的朋友會發現Itr中的也有一個remove方法,實質也是呼叫了ArrayList中的remove,但增加了expectedModCount = modCount;保證了不會丟擲java.util.ConcurrentModificationException異常。

但是,這個辦法的有兩個弊端
1.只能進行remove操作,add、clear等Itr中沒有。
2.而且只適用單執行緒環境。

2. 多執行緒環境

在多執行緒環境下,我們再次試驗下上面的程式碼

public class Test2 {
    static List<String> list = new ArrayList<String>();

    public static void main(String[] args) {
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");

        new Thread() {
            public void run() {
                Iterator<String> iterator = list.iterator();

                while (iterator.hasNext()) {
                    System.out.println(Thread.currentThread().getName() + ":"
                            + iterator.next());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
        }.start();

        new Thread() {
            public synchronized void run() {
                Iterator<String> iterator = list.iterator();

                while (iterator.hasNext()) {
                    String element = iterator.next();
                    System.out.println(Thread.currentThread().getName() + ":"
                            + element);
                    if (element.equals("c")) {
                        iterator.remove();
                    }
                }
            };
        }.start();

    }
}

 

異常的原因很簡單,一個執行緒修改了list的modCount導致另外一個執行緒迭代時modCount與該迭代器的expectedModCount不相等。

此時有兩個辦法:

1,迭代前加鎖,解決了多執行緒問題,但還是不能進行迭代add、clear等操作

public class Test2 {
 static List<String> list = new ArrayList<String>();

 public static void main(String[] args) {
     list.add("a");
     list.add("b");
     list.add("c");
     list.add("d");

     new Thread() {
         public void run() {
             Iterator<String> iterator = list.iterator();

             synchronized (list) {
                 while (iterator.hasNext()) {
                     System.out.println(Thread.currentThread().getName()
                             + ":" + iterator.next());
                     try {
                         Thread.sleep(1000);
                     } catch (InterruptedException e) {
                         // TODO Auto-generated catch block
                         e.printStackTrace();
                     }
                 }
             }
         };
     }.start();

     new Thread() {
         public synchronized void run() {
             Iterator<String> iterator = list.iterator();

             synchronized (list) {
                 while (iterator.hasNext()) {
                     String element = iterator.next();
                     System.out.println(Thread.currentThread().getName()
                             + ":" + element);
                     if (element.equals("c")) {
                         iterator.remove();
                     }
                 }
             }
         };
     }.start();

 }
}

 2,採用CopyOnWriteArrayList,解決了多執行緒問題,同時可以add、clear等操作

public class Test2 {
 static List<String> list = new CopyOnWriteArrayList<String>();

 public static void main(String[] args) {
     list.add("a");
     list.add("b");
     list.add("c");
     list.add("d");

     new Thread() {
         public void run() {
             Iterator<String> iterator = list.iterator();

                 while (iterator.hasNext()) {
                     System.out.println(Thread.currentThread().getName()
                             + ":" + iterator.next());
                     try {
                         Thread.sleep(1000);
                     } catch (InterruptedException e) {
                         // TODO Auto-generated catch block
                         e.printStackTrace();
                     }
                 }
         };
     }.start();

     new Thread() {
         public synchronized void run() {
             Iterator<String> iterator = list.iterator();

                 while (iterator.hasNext()) {
                     String element = iterator.next();
                     System.out.println(Thread.currentThread().getName()
                             + ":" + element);
                     if (element.equals("c")) {
                         list.remove(element);
                     }
                 }
         };
     }.start();

 }
}

 CopyOnWriteArrayList也是一個執行緒安全的ArrayList,其實現原理在於,每次add,remove等所有的操作都是重新建立一個新的陣列,再把引用指向新的陣列。

深入理解異常—fail-fast機制

到這裡,我們似乎已經理解完這個異常的產生緣由了。
但是,仔細思考,還是會有幾點疑惑:

  1. 既然modCount與expectedModCount不同會產生異常,那為什麼還設定這個變數
  2. ConcurrentModificationException可以翻譯成“併發修改異常”,那這個異常是否與多執行緒有關呢?

我們來看看原始碼中modCount的註解

/**
     * The number of times this list has been <i>structurally modified</i>.
     * Structural modifications are those that change the size of the
     * list, or otherwise perturb it in such a fashion that iterations in
     * progress may yield incorrect results.
     *
     * <p>This field is used by the iterator and list iterator implementation
     * returned by the {@code iterator} and {@code listIterator} methods.
     * If the value of this field changes unexpectedly, the iterator (or list
     * iterator) will throw a {@code ConcurrentModificationException} in
     * response to the {@code next}, {@code remove}, {@code previous},
     * {@code set} or {@code add} operations.  This provides
     * <i>fail-fast</i> behavior, rather than non-deterministic behavior in
     * the face of concurrent modification during iteration.
     *
     * <p><b>Use of this field by subclasses is optional.</b> If a subclass
     * wishes to provide fail-fast iterators (and list iterators), then it
     * merely has to increment this field in its {@code add(int, E)} and
     * {@code remove(int)} methods (and any other methods that it overrides
     * that result in structural modifications to the list).  A single call to
     * {@code add(int, E)} or {@code remove(int)} must add no more than
     * one to this field, or the iterators (and list iterators) will throw
     * bogus {@code ConcurrentModificationExceptions}.  If an implementation
     * does not wish to provide fail-fast iterators, this field may be
     * ignored.
     */
    protected transient int modCount = 0;

 我們注意到,註解中頻繁的出現了fail-fast
那麼fail-fast(快速失敗)機制是什麼呢?

“快速失敗”也就是fail-fast,它是Java集合的一種錯誤檢測機制。當多個執行緒對集合進行結構上的改變的操作時,有可能會產生fail-fast機制。記住是有可能,而不是一定。例如:假設存在兩個執行緒(執行緒1、執行緒2),執行緒1通過Iterator在遍歷集合A中的元素,在某個時候執行緒2修改了集合A的結構(是結構上面的修改,而不是簡單的修改集合元素的內容),那麼這個時候程式就會丟擲 ConcurrentModificationException 異常,從而產生fail-fast機制。

 

看到這裡,我們明白了,fail-fast機制就是為了防止多執行緒修改集合造成併發問題的機制嘛。
之所以有modCount這個成員變數,就是為了辨別多執行緒修改集合時出現的錯誤。而java.util.ConcurrentModificationException就是併發異常。
但是單執行緒使用不單時也可能丟擲這個異常。

 

相關文章