快速失敗機制--fail-fast

fail-fast 機制是Java集合(Collection)中的一種錯誤機制。當多個線程對同一個集合的內容進行操做時,就可能會產生fail-fast(快速失敗)事件。例如:當某一個線程A經過iterator去遍歷某集合的過程當中,若該集合的內容被其餘線程所改變了;那麼線程A訪問集合時,就會拋出ConcurrentModificationException異常,產生fail-fast事件。java

迭代器的快速失敗行爲沒法獲得保證,它不能保證必定會出現該錯誤,可是快速失敗操做會盡最大努力拋出ConcurrentModificationException異常。數組

注意:上面所說的是在多線程環境下會發生fail-fast事件,可是單線程條件下若是違反了規則也是會產生fail-fast事件的多線程

在文檔中有這麼一段話:編寫的程序依賴於快速失敗機制產生的異常是不對的,迭代器的快速檢測機制僅僅用於檢測錯誤。ide

分別用兩段程序測試快速失敗機制產生的緣由:

單線程環境:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * Created with IDEA
 *
 * @author DuzhenTong
 * @Date 2018/3/18
 * @Time 17:34
 */
public class FailFast {
    public static void main(String[] args) {
        List list = new ArrayList();
        for (int i = 0; i < 10; i++) {
            list.add(i);
        }
        iterator(list);
    }

    public static void iterator(List list) {
        Iterator it = list.iterator();
        int index = 0;
        while (it.hasNext()) {
            if (index == 6) {
                list.remove(index);
            }
            index++;
            System.out.println(it.next());
        }
    }
}

輸出結果:測試

0
1
2
3
4
5
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:819)
    at java.util.ArrayList$Itr.next(ArrayList.java:791)
    at FailFast.iterator(FailFast.java:29)
    at FailFast.main(FailFast.java:18)
多線程環境:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * Created with IDEA
 *
 * @author DuzhenTong
 * @Date 2018/3/18
 * @Time 17:59
 */
public class FailFast1 {

    public static List list = new ArrayList();
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            list.add(i);
        }
        new ThreadA().start();
        new ThreadB().start();
    }

    public static class ThreadA extends Thread {
        @Override
        public void run() {
            Iterator it = list.iterator();
            while (it.hasNext()) {
                System.out.println("集合遍歷中……:"+it.next());
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static class ThreadB extends Thread {
        @Override
        public void run() {
            int index = 0;
            while (index != 10) {
                System.out.println("線程等待中……:"+index);
                if (index == 3) {
                    list.remove(index);
                }
                index++;
            }
        }
    }
}

輸出結果:this

線程等待中……:0
集合遍歷中……:0
線程等待中……:1
線程等待中……:2
線程等待中……:3
線程等待中……:4
線程等待中……:5
線程等待中……:6
線程等待中……:7
線程等待中……:8
線程等待中……:9
Exception in thread "Thread-0" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:819)
    at java.util.ArrayList$Itr.next(ArrayList.java:791)
    at FailFast1$ThreadA.run(FailFast1.java:28)

上面的程序已經說明了爲何會發生fail-fast事件(快速失敗),在多線程條件下,一個線程正在遍歷集合中的元素,這時候另外一個線程更改了集合的結構,程序纔會拋出ConcurrentModificationException,在單線程條件下也是在遍歷的時候,這時候更改集合的結構,程序就會拋出ConcurrentModificationException。
要具體知道爲何會出現fail-fast,就要分析源碼,fail-fast出現是在遍歷集合的時候出現的,也就是對集合進行迭代的時候,對集合進行迭代的時候都是操做迭代器,集合中的內部類:(ArrayList源碼).net

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

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            //………此處代碼省略…………
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

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

上面的代碼中咱們能夠看到拋出ConcurrentModificationException異常,上面的next(),remove()都會調用checkForComodification()方法檢查兩個變量的值是否相等,不相等就會拋出異常。在上面程序中的數字1處:線程

int expectedModCount = modCount;

modCount的值賦值給expectedModCount,知道modCount這個值的含義是什麼?爲何會發生改變?緣由就會找到了。
源碼點進去,發現這個modCount變量並不在ArrayList類中,而在AbstractList中,做爲一個成員變量。code

protected transient int modCount = 0;

接下來分析源碼,看最經常使用的方法對象

ArrayList中add方法:
public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

調用的ensureCapacityInternal方法:

private void ensureCapacityInternal(int minCapacity) {
        modCount++;//modCount自增————————————
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
ArrayList中的remove方法:
public E remove(int index) {
        rangeCheck(index);

        modCount++;//modCount自增——————————————
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // Let gc do its work

        return oldValue;
    }
ArrayList中的clear方法:
public void clear() {
        modCount++;//modCount自增——————————————

        // Let gc do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }

分析了源碼就知道緣由是什麼了,凡是涉及到改變了集合的結構(改變元素的個數)的操做(包括增長,移除或者清空等)modCount這個變量都會自增,在得到迭代對象的時候,先把這個modCount變量賦值給expectedModCount,在迭代的時候每次都會檢查這個變量是否與expectedModCount一致,由於若是是在集合中添加或者刪除元素modCount的值都會發生改變。

解決方法:

  • 對於涉及到更改集合中元素個數的操做統統加上synchronized,或者利用Collections.synchronizedList強制他們的操做都是同步的。
  • 使用CopyOnWriteArrayList來替換ArrayList

這裏在網上看到不少的文章都是這麼說的:爲何CopyOnWriteArrayList能夠作到不會發生fail-fast?由於CopyOnWriteArrayList全部可變操做(add、set 等等)都是經過對底層數組進行一次新的複製來實現的。

能夠分析源碼(下面的1,2處)

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

個人見解:緣由不止有CopyOnWriteArrayList的add、set、remove等會改變原數組的方法中,都是先copy一份原來的array,再在copy數組上進行add、set、remove操做,這就纔不影響COWIterator那份數組。

爲何沒有記錄修改次數的值或者說不比較modCount也能作到內存的一致性呢?
在上面的代碼1處,調用了getArray()方法,看源碼:

final Object[] getArray() {
        return array;
    }
private volatile transient Object[] array;

由於getArray()返回的array的類型是用volatile修飾的,volatile類型的(強制內存一致性)
具體能夠看個人另外一篇關於volatile的:volatile關鍵字解析

參考文章:http://cmsblogs.com/?p=1220

相關文章
相關標籤/搜索