基於源碼理解通透Iterator迭代器的Fail-Fast快速失敗與Fail-Safe安全失敗機制

image

原創/朱季謙

在Java編程當中,Iterator迭代器是一種用於遍歷如List、Set、Map等集合的工具。這類集合部分存在線程安全的問題,例如ArrayList,若在多線程環境下,迭代遍歷過程中存在其他線程對這類集合進行修改的話,就可能導致不一致或者修改異常問題,因此,針對這種情況,迭代器提供了兩種處理策略:Fail-Fast(快速失敗)和Fail-Safe(安全失敗)。

先簡單介紹下這兩種策略——

1. Fail-Fast(快速失敗)機制
快速失敗機制是指集合在迭代遍歷過程中,其他多線程或者當前線程對該集合進行增加或者刪除元素等操作,當前線程迭代器讀取集合時會立馬拋出一個ConcurrentModificationException異常,避免數據不一致。實現原理是迭代器在創建時,會獲取集合的計數變量當作一個標記,迭代過程中,若發現該標記大小與計數變量不一致了,就以爲集合做了新增或者刪除等操作,就會拋出快速失敗的異常。在ArrayList默認啓用該機制。

2. Fail-Safe(安全失敗)機制
安全失敗機制是指集合在迭代遍歷過程中,若其他多線程或者當前線程對該集合進行修改(增加、刪除等元素)操作,當前線程迭代器仍然可以正常繼續讀取集合遍歷,而不會拋出異常。該機制的實現,是通過迭代器在創建時,對集合進行了快照操作,即迭代器遍歷的是原集合的數組快照副本,若在這個過程,集合進行修改操作,會將原有的數組內容複製到新數組上,並在新數組上進行修改,修改完成後,再將集合數組的引用指向新數組,,而讀取操作仍然是訪問舊的快照副本,故而實現讀寫分離,保證讀取操作的線程安全性。在CopyOnWriteArrayList默認啓用該機制。

基於這兩個策略,分別寫一個案例來說明。

一、迭代器的Fail-Fast(快速失敗)機制原理

Fail-Fast(快速失敗)機制案例,用集合ArrayList來說明,這裏用一個線程就能模擬出該機制——

  public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("張三");
        list.add("李四");
        list.add("王五");
        Iterator iterator = list.iterator();
        while(iterator.hasNext()) {
            //第一次遍歷到這裏,能正常打印,第二次遍歷到這裏,因上一次遍歷做了list.add("李華")操作,集合已經改變,故而出現Fail-Fast(快速失敗)異常
            String item = (String)iterator.next();
            list.add("李華");
            System.out.println(item);
        }
        System.out.println(list);
    }

執行這段代碼,打印日誌出現異常ConcurrentModificationException,說明在遍歷過程當中,操作 list.add("李華")對集合做新增操作後,就會出現Fail-Fast(快速失敗)機制,拋出異常,阻止繼續進行遍歷——

張三
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
	at java.util.ArrayList$Itr.next(ArrayList.java:861)
	at ListExample.IteratorTest.main(IteratorTest.java:23)

這裏面是怎麼實現該Fail-Fast(快速失敗)機制的呢?

先來看案例裏創建迭代器的這行代碼Iterator iterator = list.iterator(),底層是這樣的——

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

Itr類是ArrayList內部類,實現了Iterator 接口,說明它本質是ArrayList內部一個迭代器。這裏省略部分暫時無關緊要的代碼,只需關注hasNext()和next()即可——

  private class Itr implements Iterator<E> {
        int cursor;       // 迭代計數器
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;
        //判斷是否已經迭代到最後一位
        public boolean hasNext() {
            return cursor != size;
        }
			
    		//取出當前遍歷到集合元素
			  public E next() {
          	//判斷集合是否有做新增或者刪除操作
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }
    		......
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
}

再進入案例裏的這行代碼 String item = (String)iterator.next()底層,也就是Itr類的public E next() {......}方法。

注意next()裏的這個方法 checkForComodification(),進入到方法裏,可以看到,ConcurrentModificationException異常正是在這個方法裏拋出來的,它做了一個判斷,判斷modCount是否等於expectedModCount,若不等於,就拋出快速失敗異常。

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

那麼,問題就簡單了,研究ArrayList快速失敗機制,本質只需要看modCount和expectedModCount是什麼,就知道ArrayList的Fail-Fast(快速失敗)機制是怎麼處理的了。

在內部類Itr中,定義int expectedModCount = modCount,說明expectedModCount是在迭代器new Itr()創建時,就將此時的modCount數值賦值給變量expectedModCount,意味着,在整個迭代器生命週期內,這個expectedModCount是固定的了,從變量名就可以看出,它表示集合預期修改的次數,而modCount應該就是表示列表修改次數。假如迭代器創建時,modCount修改次數是5,那麼整個迭代器生命週期內,預期的修改次數expectedModCount就只能等於5。

請注意最爲關鍵的一個地方,modCount是可以變的。

先看一下在ArrayList裏,這個modCount是什麼?

這個modCount是定義在ArrayList的父類AbstractList裏的——

/**
 *這個列表在結構上被修改的次數。結構修改是指改變列表,或者以其他方式擾亂它,使其迭代進步可能產生不正確的結果。
 *
 *該字段由迭代器和列表迭代器實現使用,由{@code迭代器}和{@code listtiterator}方法返回。
 *如果該字段的值發生了意外變化,迭代器(或列表)將返回該字段迭代器)將拋出{@code ConcurrentModificationException} 
 *在響應{@code next}, {@code remove}, {@code previous},{@code set}或{@code add}操作。這提供了快速故障行爲。
 *
 */
protected transient int modCount = 0;

根據註釋,可以得知,這是一個專門記錄列表被修改的次數,在ArrayList當中,涉及到add新增、remove刪除、fastRemove、clear等涉及列表結構改動的操作,,都會通過modCount++形式,增加列表在結構上被修改的次數。

modCount表示列表被修改的次數。

我們在案例代碼裏,做了add操作——

while(iterator.hasNext()) {
    String item = (String)iterator.next();
    list.add("李華");
    System.out.println(item);
}

進入到ArrayList的add方法源碼裏,可以看到,在add新增過程中,按照ensureCapacityInternal =》ensureExplicitCapacity執行順序,最後通過modCount++修改了變量modCount——

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


 private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
 }

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

// overflow-conscious code
	if (minCapacity - elementData.length > 0)
    grow(minCapacity);
}

總結一下,迭代器創建時,變量expectedModCount是被modCount賦值,在整個迭代器等生命週期中,變量expectedModCount值是固定的了,但在第一輪遍歷過程中,通過list.add("李華")操作,導致modCount++,最終就會出現expectedModCount != modCount。因此,在迭代器進行第二輪遍歷時,執行到 String item = (String)iterator.next(),在next()裏調用checkForComodification() 判斷expectedModCount是否還等於modCount,這時已經不等於,故而就會拋出ConcurrentModificationException異常,立刻結束迭代器遍歷,避免數據不一致。

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

以上,就是集合迭代器的Fail-Fast機制原理。
image


二、迭代器的Fail-Safe(安全失敗)機制原理

Fail-Fast(快速失敗)機制案例,用集合CopyOnWriteArrayList來說明,這裏用一個線程就能模擬出該機制——

   public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        list.add("張三");
        list.add("李四");
        list.add("王五");
        Iterator iterator = list.iterator();
        while(iterator.hasNext()) {
            String item = (String)iterator.next();
            list.add("李華");
            System.out.println(item);
        }
        System.out.println("最後全部打印集合結果:" + list);
    }

執行這段代碼,正常打印結果,說明在迭代器遍歷過程中,對集合做了新增元素操作,並不影響迭代器遍歷,新增的元素不會出現在迭代器遍歷當中,但是,在迭代器遍歷完成後,再一次打印集合,可以看到新增的元素已經在集合裏了——

張三
李四
王五
最後全部打印集合結果:[張三, 李四, 王五, 李華, 李華, 李華]

Fail-Safe(安全失敗)機制在CopyOnWriteArrayList體現,可以理解成,這是一種讀寫分離的機制。

下面就看一下CopyOnWriteArrayList是如何實現讀寫分離的。

先來看迭代器的創建Iterator iterator = list.iterator(),進入到list.iterator()底層源碼——

public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}

這裏的COWIterator是一個迭代器,關鍵有一個地方,在創建迭代器對象,調用其構造器時傳入兩個參數,分別是getArray()和0。

這裏的getArray()方法,獲取到一個array數組,它是CopyOnWriteArrayList集合真正存儲數據的地方。

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

另一個參數0,表示迭代器遍歷的索引值,剛開始,肯定是從數組下標0開始。

明白getArray()和0這兩個參數後,看一下迭代器創建new COWIterator(getArray(), 0)的情況,只需關注與本文有關的代碼即可,其他暫時省略——

static final class COWIterator<E> implements ListIterator<E> {
    //列表快照
    private final Object[] snapshot;
    //調用next返回的元素的索引
    private int cursor;

    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }

    public boolean hasNext() {
        return cursor < snapshot.length;
    }

    ......

    @SuppressWarnings("unchecked")
    public E next() {
        if (! hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }
}

在代碼案例中,迭代器遍歷過程時,通過hasNext()判斷集合是否遍歷完成,若還有沒遍歷的元素,就會調用 String item = (String)iterator.next()取出集合對應索引的元素。

從COWIterator類的next()方法中,可以看到,其元素是根據索引cursor從數組snapshot中取出來的。

這個snapshot就相當一個快照副本,在創建迭代器時,即new COWIterator(getArray(), 0),通過getArray()將此時CopyOnWriteArrayList集合的array數組引用複製給COWIterator的數組snapshot,那麼snapshot引用和array引用都將指向同一個數組地址了。

只需保證snapshot指向的數組地址元素不變,那麼整個迭代器讀取集合數組就不會受影響。
image

如何做到snapshot指向的數組地址元素不變,但是又需要同時能滿足CopyOnWriteArrayList集合的新增或者刪除操作呢?

先來看一下CopyOnWriteArrayList的 list.add("李華")操作,具體實現能夠在這塊源碼裏看到,主要以下步驟:

1、add方法用到了ReentrantLock鎖,在進行新增過程中,通過lock鎖保證線程安全。

2、Object[] elements = getArray()這裏的getArray()方法,和創建迭代器傳的參數getArray()是同一個,都是獲取到CopyOnWriteArrayList的array數組。取出array數組以及計算其長度後,創建一個比array數組長度大1的新數組,通過Arrays.copyOf(elements, len + 1)將array數組元素全部複製到新數組newElements。

3、在新數組newElements進行新增元素操作。

4、將CopyOnWriteArrayList的array數組引用指向新數組newElements,這樣array=newElements,完成新增操作。

  public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
          	//獲取到CopyOnWriteArrayList的array數組
            Object[] elements = getArray();
          	//獲取array數組長度
            int len = elements.length;
            //將array數組數據,全部複製到一個長度比舊數組多1的新數組裏
            Object[] newElements = Arrays.copyOf(elements, len + 1);
          	//在新數組裏,新增一個元素
            newElements[len] = e;
          	//將CopyOnWriteArrayList的array數組引用指向新數組newElements
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

可見,CopyOnWriteArrayList實現讀寫分離的原理,就是在COWIterator迭代器創建時,將此時的array數組指向的地址複製給snapshot,相當做了一次快照,迭代器遍歷該快照數組地址元素。

後續涉及到列表修改相關的操作,會將原始array數組全部元素複製到一個新數組上,在新數組裏面進行修改操作,這樣就不會影響到迭代器遍歷原來的數組地址裏的數據了。(這也表明,這種讀寫分離只適合讀多寫少,在寫多情況下,會出現性能問題)

新數組修改完畢後,只需將array數組引用指向新數組地址,就能完成修改操作了。
image

整個過程就能完成讀寫分離機制,即迭代器的Fail-Safe(安全失敗)機制。

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