1. 程式人生 > >ConcurrentModificationException和fail-fast機制

ConcurrentModificationException和fail-fast機制

單執行緒下:

1. ConcurrentModificationException出現的原因

例子:

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

public class Test {
    public static void main(String[] args)  {
        List<Integer> list = new ArrayList<Integer>();
        list.add(3);
        Iterator<Integer> iterator = list.iterator();
        while
(iterator.hasNext()){ Integer integer = iterator.next(); if(integer == 3) list.remove(integer); } } }

執行結果:

Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(Unknown Source)
    at java.util
.ArrayList$Itr.next(Unknown Source) at leetcode.Test.main(Test.java:13)

這時出現了ConcurrentModificationException,官方文件中關於這個異常的解釋如下:

public class ConcurrentModificationException extends RuntimeException
This exception may be thrown by methods that have detected concurrent modification
 of an object when such modification is
not permissible. For example, it is not generally permissible for one thread to modify a Collection while another thread is iterating over it. In general, the results of the iteration are undefined under these circumstances. Some Iterator implementations (including those of all the general purpose collection implementations provided by the JRE) may choose to throw this exception if this behavior is detected. Iterators that do this are arbitrary, non-deterministic behavior at an undetermined time in the future. ......

程式在對 collection 進行迭代時,某個執行緒對該 collection 在結構上對其做了修改,這時迭代器就會丟擲 ConcurrentModificationException 異常資訊,從而產生 fail-fast。

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

2. 解決辦法

我們可先看看以從Iterator()方法的具體實現:

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

private class Itr implements Iterator<E> {
    int cursor = 0;
    int lastRet = -1;
    int expectedModCount = modCount;
    public boolean hasNext() {
           return cursor != size();
    }
    public E next() {
           checkForComodification();
        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();
    }
}

這段程式碼不想多說,可以看到next()的checkForComodification()方法中會判斷modCount和 expectedModCount的值是否相等。如果你在集合遍歷過程中,刪除某個元素的話,會使得modCount != expectedModCount,從而丟擲ConcurrentModificationException()。

解決辦法:細心的朋友會發現Itr中有remove方法,我們可以呼叫這個方法,實現刪除元素的操作,從而不會引起ConcurrentModificationException。Iterator.remove() 方法會在刪除當前迭代物件的同時維護索引的一致性。

public class Test {
    public static void main(String[] args)  {
        List<Integer> list = new ArrayList<Integer>();
        list.add(3);
        Iterator<Integer> iterator = list.iterator();
        while(iterator.hasNext()){
            Integer integer = iterator.next();
            if(integer == 3)
                iterator.remove();//使用Iterator的remove方法
        }
    }
}

多執行緒下:

1. 出現原因

多執行緒下同樣會出現ConcurrentModificationException(),例如:

public class Test {
    static ArrayList<Integer> list = new ArrayList<Integer>();
    public static void main(String[] args)  {
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        Thread thread1 = new Thread(){
            public void run() {
                Iterator<Integer> iterator = list.iterator();
                while(iterator.hasNext()){
                    Integer integer = iterator.next();
                    System.out.println(integer);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
        };
        Thread thread2 = new Thread(){
            public void run() {
                Iterator<Integer> iterator = list.iterator();
                while(iterator.hasNext()){
                    Integer integer = iterator.next();
                    if(integer==2)
                        iterator.remove(); 
                }
            };
        };
        thread1.start();
        thread2.start();
    }
}

這段程式碼中thread1和thread2同時用iterator 對List對集合進行遍歷,同樣會產生ConcurrentModificationException()。產生的原因和單執行緒時是類似的,同樣是兩個執行緒的非同步操作使得expectedModCount和modCount的值不一樣,從而產生fail-fast。

2. 解決辦法

1)在使用iterator迭代的時候使用synchronized或者Lock進行同步;
2)使用併發容器CopyOnWriteArrayList代替ArrayList和Vector。(推薦使用)

CopyOnWriterArrayList所代表的核心概念就是:任何對array在結構上有所改變的操作(add、remove、clear等),CopyOnWriterArrayList都會copy現有的資料,再在copy的資料上修改,這樣就不會影響COWIterator中的資料了,修改完成之後改變原有資料的引用即可。同時這樣造成的代價就是產生大量的物件,同時陣列的copy也是相當有損耗的,故使用CopyOnWriterArrayList會使效率有所下降。

關於CopyOnWriteArrayList可以參照:https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/CopyOnWriteArrayList.html

這部分我現在瞭解的不是很透徹,以後再來補充。


注意事項:

  1. Java中增強for迴圈(for each),底層的實現是通過Iterator迭代器模式實現的,所以它也會產生ConcurrentModificationException。

  2. 迭代器的快速失敗行為無法得到保證,因為一般來說,不可能對是否出現不同步併發修改做出任何硬性保證。快速失敗迭代器會盡最大努力丟擲 ConcurrentModificationException。因此,為提高這類迭代器的正確性而編寫一個依賴於此異常的程式是錯誤的做法:迭代器的快速失敗行為應該僅用於檢測 bug。


參考資料:

  1. http://www.cnblogs.com/dolphin0520/p/3933551.html
  2. http://www.hollischuang.com/archives/1776
  3. https://docs.oracle.com/javase/7/docs/api/java/util/ConcurrentModificationException.html
  4. http://blog.csdn.net/chenssy/article/details/38151189