1. 程式人生 > >併發程式設計實戰(6)fail-fast機制與ConcurrentModificationException

併發程式設計實戰(6)fail-fast機制與ConcurrentModificationException

併發程式設計實戰(6): fail-fast機制與ConcurrentModificationException

ConcurrentModificationException異常

java.util.ConcurrentModificationException是一個非常常見的異常,常見於對List、Map等集合的操作當中。那麼,這個異常是什麼呢?又為什麼會產生這個異常呢?

隨便開啟ArrayList或者HashMap的原始碼(這裡用的jdk1.8)可以看到,在其一個內部類迭代器的幾乎所有方法實現中,都有對這個異常的丟擲。

/**
     * An optimized version of AbstractList.Itr
     */
private class Itr implements Iterator<E> { int cursor; // index of next element to return int lastRet = -1; // index of last element returned; -1 if no such int expectedModCount = modCount; Itr() {} public boolean hasNext() { return cursor !=
size; } @SuppressWarnings("unchecked") public E next() { ... throw new ConcurrentModificationException(); } public void remove() { ... throw new ConcurrentModificationException(); } } .
..

既然這個異常如此頻繁的丟擲,在編寫程式碼過程成遇到這個問題也就不奇怪了,提醒我要重視這個異常。那麼,它到底是什麼呢?開啟原始碼頂部作者註釋,可以看到作者的解釋(以ArrayList為例子):

* <p><a name="fail-fast">
 * The iterators returned by this class's {@link #iterator() iterator} and
 * {@link #listIterator(int) listIterator} methods are <em>fail-fast</em>:</a>
 * if the list is structurally modified at any time after the iterator is
 * created, in any way except through the iterator's own
 * {@link ListIterator#remove() remove} or
 * {@link ListIterator#add(Object) add} methods, the iterator will throw a
 * {@link ConcurrentModificationException}.  Thus, in the face of
 * concurrent modification, the iterator fails quickly and cleanly, rather
 * than risking arbitrary, non-deterministic behavior at an undetermined
 * time in the future.
 *
 * <p>Note that the fail-fast behavior of an iterator cannot be guaranteed
 * as it is, generally speaking, impossible to make any hard guarantees in the
 * presence of unsynchronized concurrent modification.  Fail-fast iterators
 * throw {@code ConcurrentModificationException} on a best-effort basis.
 * Therefore, it would be wrong to write a program that depended on this
 * exception for its correctness:  <i>the fail-fast behavior of iterators
 * should be used only to detect bugs.</i>

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

介紹中出現了多次fail-fast機制,這個後面會介紹。這種“及時失敗”的迭代器並不是一種完備的處理機制,可以看作只是一種“善意的”提醒,因此只能作為併發問題的預警指示器。在迭代器過程中,無法對集合進行修改(可以進行remove)。

Fail-Fast機制

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

說白了,fail-fast機制就是為了防止多執行緒修改集合造成併發問題的機制。

上面一直說是多執行緒環境下,也就是說是為了併發而準備的。但是要注意,單執行緒環境下也可能會出現問題,如下面的程式碼:

List<String> l = new ArrayList<>();
        l.addAll(Arrays.asList("a", "b", "c", "d", "e"));
        Iterator<String> it = l.iterator();
        while(it.hasNext()){
            String str = it.next();
            if (str.equals("b"))
                it.remove();
        }

可以發現,在迭代的時候,只能進行remove操作,沒有辦法進行別的操作,比如add等。多執行緒環境下,remove就會報錯了。

private static final int THREADNUM = 3;
public static void main(String[] args) {
    List<String> l = new ArrayList<>();
    l.addAll(Arrays.asList("a", "b", "c", "d", "e"));
    ExecutorService es = Executors.newCachedThreadPool();
    for (int i = 0; i < THREADNUM; i++) {
        es.execute(() -> {
            try {
                Iterator<String> it = l.iterator();
                while (it.hasNext()) {
                    String str = it.next();
                    System.out.println(str);
                    if (str.equals("b"))
                        it.remove();
                    }
             } catch (Exception e){
                 e.printStackTrace();
             }
        });
    }
}

從原始碼理解fail-fast產生原因

private class Itr implements Iterator<E> {
        int cursor;
        int lastRet = -1;
        int expectedModCount = ArrayList.this.modCount;
 
        public boolean hasNext() {
            return (this.cursor != ArrayList.this.size);
        }
 
        public E next() {
            checkForComodification();
            /** 省略此處程式碼 */
        }
 
        public void remove() {
            if (this.lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();
            /** 省略此處程式碼 */
        }
 
        final void checkForComodification() {
            if (ArrayList.this.modCount == this.expectedModCount)
                return;
            throw new ConcurrentModificationException();
        }
    }

從原始碼中可以看出,迭代器中的方法都會呼叫checkForComodification()這個方法來進行檢查。方法原始碼如下:

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

可以看到,這個方法就是簡單的對modCount和expectedModCount是否相同進行檢查。如果兩者不相等,就會丟擲這個異常。另外,由於int expectedModCount = ArrayList.this.modCount; 所以,expectedModCount 這個值是不會變的,變得只有modeCount,那麼它何時改變呢?

在AbstractList中定義了這個變數。

protected transient int modCount = 0;

看Arraylist原始碼可以知道,add、remove等方法都會對modCount進行增加。當expectedModCount 與modCount的改變不同步時,就會導致兩者不相等,造成異常。 舉例如下:

有兩個執行緒(執行緒A,執行緒B),其中執行緒A負責遍歷list、執行緒B修改list。執行緒A在遍歷list過程的某個時候(此時expectedModCount = modCount=N),執行緒啟動,同時執行緒B增加一個元素,這是modCount的值發生改變(modCount + 1 = N + 1)。執行緒A繼續遍歷執行next方法時,通告checkForComodification方法發現expectedModCount = N ,而modCount = N + 1,兩者不等,這時就丟擲ConcurrentModificationException 異常,從而產生fail-fast機制。

解決辦法也有很多,後面學的深入了再做介紹。