剖析fail-fast機制和ConcurrentModificationException

快速失敗,是java集合中的一種錯誤檢測機制。當多個線程對集合進行結構上的改變操作時,就有可能會產生fail-fast機制。例如存在兩個線程1和2,線程1通過Iterator在遍歷集合中元素時,線程2修改了集合的結構(添加或者刪除元素),這個時候就會拋出ConcurrentModificationException異常,從而產生了fail-fast機制。
併發修改異常ConcurrentModificationException:方法檢測到對象的併發修改時,但是不允許這種修改,就會拋出該異常。單線程和多線程時都有可能產生這種異常。

分析ArrayList源碼可知,迭代器在調用next() 和remove()方法時,都會調用checkForComodification()方法,該方法主要就是檢測modCount和expectedModCount是否相等。如果不相等則拋出異常,從而產生了fail-fast機制。
modCount用來記錄集合修改的次數,每修改一次(添加或者刪除),modCount++。

具體分析如下:
線程1在遍歷集合A。調用了集合的iterator方法。

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

Itr是ArrayList的內部類,實現了Iterator接口

 private class Itr implements Iterator<E> {
        int cursor;      
        int lastRet = -1;        
int expectedModCount = modCount;

        ………省略

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            ………省略        
}

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();
            ………省略
        }

        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            ………省略
            checkForComodification();
        }

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

由上述可知,在調用remove和next方法時都會調用checkForComodification()方法。
看看checkForComodification()方法的實現,代碼如下:

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

這裏進行了modCount 和expectedModCount的比較。如果不相等,則拋出ConcurrentModificationException異常。
下面分析一下什麼時候纔會不相等?

private class Itr implements Iterator<E> {
        int expectedModCount = modCount;

在新建itr對象時,會把當前的modCount的值傳遞給expectedModCount,之後, expectedModCount的值就不會再改變。因此下面要分析一下modCount什麼時候會發生改變?以add方法爲例

public void add(int index, E e) {
            rangeCheckForAdd(index);
            checkForComodification();
            parent.add(parentOffset + index, e);
           //修改modCount的值
            this.modCount = parent.modCount;
            this.size++;
        }

觀察源碼可知,add remove clear方法,只要涉及到修改集合結構時,就會改變modCount的值。
繼續剛纔線程1的執行,假設這時線程2執行了add方法,向集合中添加了一個元素,此時modCount++,線程1接着遍歷,在執行到next函數時,調用checkForComodification方法比較expectedModCount和modCount的值,發現不相等了,就會拋出併發修改異常。從而產生了fail-fast機制。

如何解決fail-fast呢?或者解決併發修改異常呢?
可以使用同步來解決,在客戶端調用會改變modCount值的方法時,加synchronized,或者直接使用Collections.synchronizedList類。
還可以使用jdk5提供的CopyOnWriteArrayList類。
CopyOnWriteArrayList僅僅實現了List集合接口,並沒有繼承AbstractList抽象類。ArrayList的iterator()方法是繼承了AbstractList,但是CopyOnWriteArrayList是自己實現了iterator。最主要的原因是CopyOnWriteArrayList的Iterator實現類中沒有checkForComodification方法,所以不會拋出併發修改異常。
那CopyOnWriteArrayList實現的原理是什麼?以add方法爲例

public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
}

關鍵就在於

Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);

這三行代碼。它是先對原先的數組進行復制,然後在複製之後的數組上進行添加元素,最後在改變原有數據的引用即可。怪不得該類叫做CopyOnWriteArrayList,無疑是先複製再進行寫操作!

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