Java提高篇(三四)-----fail-fast機制,java-----fail-fast

來源:互聯網
上載者:User

Java提高篇(三四)-----fail-fast機制,java-----fail-fast

        在JDK的Collection中我們時常會看到類似於這樣的話:

        例如,ArrayList:

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

        HashMap中:

注意,迭代器的快速失敗行為不能得到保證,一般來說,存在非同步的並發修改時,不可能作出任何堅決的保證。快速失敗迭代器盡最大努力拋出 ConcurrentModificationException。因此,編寫依賴於此異常的程式的做法是錯誤的,正確做法是:迭代器的快速失敗行為應該僅用於檢測程式錯誤。

        在這兩段話中反覆地提到”快速失敗”。那麼何為”快速失敗”機制呢?

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

一、fail-fast樣本
public class FailFastTest {    private static List<Integer> list = new ArrayList<>();        /**     * @desc:線程one迭代list     * @Project:test     * @file:FailFastTest.java     * @Authro:chenssy     * @data:2014年7月26日     */    private static class threadOne extends Thread{        public void run() {            Iterator<Integer> iterator = list.iterator();            while(iterator.hasNext()){                int i = iterator.next();                System.out.println("ThreadOne 遍曆:" + i);                try {                    Thread.sleep(10);                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }    }        /**     * @desc:當i == 3時,修改list     * @Project:test     * @file:FailFastTest.java     * @Authro:chenssy     * @data:2014年7月26日     */    private static class threadTwo extends Thread{        public void run(){            int i = 0 ;             while(i < 6){                System.out.println("ThreadTwo run:" + i);                if(i == 3){                    list.remove(i);                }                i++;            }        }    }        public static void main(String[] args) {        for(int i = 0 ; i < 10;i++){            list.add(i);        }        new threadOne().start();        new threadTwo().start();    }}
 運行結果:

ThreadOne 遍曆:0ThreadTwo run:0ThreadTwo run:1ThreadTwo run:2ThreadTwo run:3ThreadTwo run:4ThreadTwo run:5Exception in thread "Thread-0" java.util.ConcurrentModificationException    at java.util.ArrayList$Itr.checkForComodification(Unknown Source)    at java.util.ArrayList$Itr.next(Unknown Source)    at test.ArrayListTest$threadOne.run(ArrayListTest.java:23)

二、fail-fast產生原因

        通過上面的樣本和講解,我初步知道fail-fast產生的原因就在於程式在對 collection 進行迭代時,某個線程對該 collection 在結構上對其做了修改,這時迭代器就會拋出 ConcurrentModificationException 異常資訊,從而產生 fail-fast。

        要瞭解fail-fast機制,我們首先要對ConcurrentModificationException 異常有所瞭解。當方法檢測到對象的並發修改,但不允許這種修改時就拋出該異常。同時需要注意的是,該異常不會始終指出對象已經由不同線程並發修改,如果單線程違反了規則,同樣也有可能會拋出改異常。

        誠然,迭代器的快速失敗行為無法得到保證,它不能保證一定會出現該錯誤,但是快速失敗操作會盡最大努力拋出ConcurrentModificationException異常,所以因此,為提高此類操作的正確性而編寫一個依賴於此異常的程式是錯誤的做法,正確做法是:ConcurrentModificationException 應該僅用於檢測 bug。下面我將以ArrayList為例進一步分析fail-fast產生的原因。

從前面我們知道fail-fast是在操作迭代器時產生的。現在我們來看看ArrayList中迭代器的原始碼:

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();        }    }

        從上面的原始碼我們可以看出,迭代器在調用next()、remove()方法時都是調用checkForComodification()方法,該方法主要就是檢測modCount == expectedModCount ? 若不等則拋出ConcurrentModificationException 異常,從而產生fail-fast機制。所以要弄清楚為什麼會產生fail-fast機制我們就必須要用弄明白為什麼modCount != expectedModCount ,他們的值在什麼時候發生改變的。

        expectedModCount 是在Itr中定義的:int expectedModCount = ArrayList.this.modCount;所以他的值是不可能會修改的,所以會變的就是modCount。modCount是在 AbstractList 中定義的,為全域變數:

protected transient int modCount = 0;

那麼他什麼時候因為什麼原因而發生改變呢?請看ArrayList的源碼:

    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方法只要是涉及了改變ArrayList元素的個數的方法都會導致modCount的改變。所以我們這裡可以初步判斷由於expectedModCount 得值與modCount的改變不同步,導致兩者之間不等從而產生fail-fast機制。知道產生fail-fast產生的根本原因了,我們可以有如下情境:

        有兩個線程(線程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機制。

        所以,直到這裡我們已經完全瞭解了fail-fast產生的根本原因了。知道了原因就好找解決辦法了。

三、fail-fast解決辦法

        通過前面的執行個體、源碼分析,我想各位已經基本瞭解了fail-fast的機制,下面我就產生的原因提出解決方案。這裡有兩種解決方案:

        方案一:在遍曆過程中所有涉及到改變modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList,這樣就可以解決。但是不推薦,因為增刪造成的同步鎖可能會阻塞遍曆操作。

        方案二:使用CopyOnWriteArrayList來替換ArrayList。推薦使用該方案。

        CopyOnWriteArrayList為何物?ArrayList 的一個安全執行緒的變體,其中所有可變操作(add、set 等等)都是通過對底層數組進行一次新的複製來實現的。 該類產生的開銷比較大,但是在兩種情況下,它非常適合使用。1:在不能或不想進行同步遍曆,但又需要從並發線程中排除衝突時。2:當遍曆操作的數量大大超過可變操作的數量時。遇到這兩種情況使用CopyOnWriteArrayList來替代ArrayList再適合不過了。那麼為什麼CopyOnWriterArrayList可以替代ArrayList呢?

        第一、CopyOnWriterArrayList的無論是從資料結構、定義都和ArrayList一樣。它和ArrayList一樣,同樣是實現List介面,底層使用數組實現。在方法上也包含add、remove、clear、iterator等方法。

        第二、CopyOnWriterArrayList根本就不會產生ConcurrentModificationException異常,也就是它使用迭代器完全不會產生fail-fast機制。請看:

private static class COWIterator<E> implements ListIterator<E> {        /** 省略此處代碼 */        public E next() {            if (!(hasNext()))                throw new NoSuchElementException();            return this.snapshot[(this.cursor++)];        }        /** 省略此處代碼 */    }

        CopyOnWriterArrayList的方法根本就沒有像ArrayList中使用checkForComodification方法來判斷expectedModCount 與 modCount 是否相等。它為什麼會這麼做,憑什麼可以這麼做呢?我們以add方法為例:

public boolean add(E paramE) {        ReentrantLock localReentrantLock = this.lock;        localReentrantLock.lock();        try {            Object[] arrayOfObject1 = getArray();            int i = arrayOfObject1.length;            Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1);            arrayOfObject2[i] = paramE;            setArray(arrayOfObject2);            int j = 1;            return j;        } finally {            localReentrantLock.unlock();        }    }        final void setArray(Object[] paramArrayOfObject) {        this.array = paramArrayOfObject;    }

        CopyOnWriterArrayList的add方法與ArrayList的add方法有一個最大的不同點就在於,下面三句代碼:

Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1);arrayOfObject2[i] = paramE;setArray(arrayOfObject2);

        就是這三句代碼使得CopyOnWriterArrayList不會拋ConcurrentModificationException異常。他們所展現的魅力就在於copy原來的array,再在copy數組上進行add操作,這樣做就完全不會影響COWIterator中的array了。

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

        參考文檔:http://www.cnblogs.com/skywang12345/p/3308762.html#a3

 

-----原文出自:http://cmsblogs.com/?p=1220,請尊重作者辛勤勞動成果,轉載說明出處.

-----個人網站:http://cmsblogs.com




聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.