Java集合框架——容器的快速報錯機制 fail-fast 是什麼?

前言:最近看 java 集合方面的源碼,瞭解到集合使用了 fail-fast 的機制,這裏就記錄一下這個機制是什麼,有什麼用,如何實現的。java

1、fail-fast 簡介

  fail-fast 機制,即快速失敗機制,是java集合(Collection)中的一種錯誤檢測機制。當在迭代集合的過程當中該集合在結構上發生改變的時候,就有可能會發生 fail-fast,即拋出 ConcurrentModificationException 異常。fail-fast 機制並不保證在不一樣步的修改下必定會拋出異常,它只是盡最大努力去拋出,因此這種機制通常僅用於檢測 bug。  設計模式

   fail-fast 機制出如今 java 集合的 ArrayList、HashMap 等,在多線程或者單線程裏面都有可能出現快速報錯,即出現 ConcurrentModificationException 異常。數組

2、fail-fast 有什麼用?

   爲了檢測在迭代集合的過程當中,這個集合是否發生了增長、刪除等(add、remove、clear)使結構發生變化的事。多線程

3、測試

   這個單測跑下來是成功的,下面有講解爲何。ide

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.List;

/**
 * @author yule
 * @date 2018/9/13 17:05
 */
public class ArrayListTest {
    private List<Integer> list = null;

    @Before
    public void buildList(){
        list = new ArrayList<>();
        for(int i = 0; i < 10; i++){
            list.add(i);
        }
        Assert.assertEquals("[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]", list.toString());
    }

    @Test
    public void testFor(){
        for(int i = 0; i < list.size(); i++){
            if(i == 2){
                //這個會刪除成功,這裏沒有 fail-fast 的機制
                list.remove(2);
            }
        }
        Assert.assertEquals("[0, 1, 3, 4, 5, 6, 7, 8, 9]", list.toString());
    }

    @Test(expected = ConcurrentModificationException.class)
    public void testForEach(){
        for(int x : list){
            if(x == 2){
                //這個拋出異常,這裏就是 fail-fast 的機制,畢竟 foreach 底層就是 Iterator
                list.remove(2);
            }
        }
    }

    @Test(expected = ConcurrentModificationException.class)
    public void testIterator(){
        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()){
            int x = iterator.next();
            if(x == 2){
                //這個拋出異常,這裏就是 fail-fast 的機制
                list.remove(2);
            }
        }
    }

    @Test
    public void testIteratorSuccess(){
        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()){
            int x = iterator.next();
            if(x == 2){
                //這個會成功,不會拋出異常,這裏也是 fail-fast 的機制,不知道這裏爲何會成功,能夠繼續看下去
                iterator.remove();
            }
        }  
     Assert.assertEquals("[0, 1, 3, 4, 5, 6, 7, 8, 9]", list.toString());
  } 
}

  第一個 testFor() 單測成功,是由於普通的 for 循環沒有 fail-fast 機制,由於 fail-fast 機制只針對迭代集合的過程。測試

  第二個 testForEach() 單測拋異常,是由於 forEach 底層就是使用了迭代器,其緣由和 testIterator() 單測一致。ui

  第三個 testIterator() 單測拋異常,是由於 ArrayList.remove() 方法只修改了 modCount++,而沒有修改 Itr 的 expectedModCount。(詳見下面原理)this

  第四個 testIteratorSuccess() 單測成功,是由於 ArrayList 的迭代器的 remove() 方法不只僅是修改了 modCount,也修改了 Itr 的 expectedModCount。(詳見下面原理)spa

4、fail-fast 在源碼中(ArrayList)如何實現的?(原理)

  首先,必須瞭解 fail-fast 兩個關鍵的東西,ArrayList 的 modCount 和 ArrayList.Itr 內部類的 expectedModCount。.net

  其中,modCount 是抽象類 AbstractList 中的變量,默認爲 0,而 ArrayList 繼承了 AbstractList ,因此也有這個變量,modCount 用於記錄集合操做過程當中做的修改次數,並非 size。每次 add 或者 remove modCount 都會 ++。

  其中,expectedModCount 是 ArrayList 內部類 Itr 的成員變量,初始值爲 modCount。執行迭代器的 remove、add 方法,都會先執行 ArrayList 的 remove、add 方法(modCount++),而後會執行 expectedModCount = modCount。

  上面說到 fail-fast 只針對 迭代器,因此須要知道 ArrayList 的迭代器的實現源碼:

    /**
     * Returns an iterator over the elements in this list in proper sequence.
     *
     * <p>The returned iterator is <a href="#fail-fast"><i>fail-fast</i></a>.
     *
     * @return an iterator over the elements in this list in proper sequence
     */
    public Iterator<E> iterator() {
        return new Itr();
    }

  ArrayList 的 內部類 Itr 實現了 Iterator 接口,源碼以下: 

/**
     * 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
        // 這個就是 fail-fast 判斷的關鍵變量,初始值就爲ArrayList中的modCount
        int expectedModCount = modCount;

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

        @SuppressWarnings("unchecked")
        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];
        }

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

        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }

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

  其中,cursor 是指集合遍歷過程當中的即將遍歷的元素的索引。

  lastRet 是 cursor  - 1,默認爲 -1,即不存在上一個時,爲 -1,它主要用於記錄剛剛遍歷過的元素的索引。

  迭代器迭代結束的標誌就是 hasNext() 返回 false,而該方法就是用 cursor 遊標和 size (集合中的元素數目)進行對比,當 cursor 等於 size 時,表示已經遍歷完成。

5、解惑

一、什麼是指集合在結構上發生變化呢?

  其實就是指 size 有變更,好比 ArrayList 的 add 和 delete、clear。

二、迭代集合的過程又是指什麼呢?(這裏涉及到 forEach 的原理)

  迭代集合的過程指的就是使用代迭代器 Iterator 或者 forEach 語法,實際上一個類要使用 forEach 就必須實現 Iterable 接口而且重寫它的 Iterator 方法因此 forEach 在本質上仍是使用的 Iterator。由於 forEach 原理:在編譯的時候編譯器會自動將對 for 這個關鍵字的使用轉化爲對目標的迭代器的使用。

  因此得出結論:

  一、ArrayList 之因此能使用 foreach 循環遍歷,是由於 ArrayList 全部的 List 都是 Collection 的子接口,而 Collection 是 Iterable 的子接口,ArrayList 的父類 AbstractList 正確地實現了 Iterable 接口的 iterator 方法。

  二、任何一個集合,不管是 JDK 提供的仍是本身寫的,只要想使用 foreach 循環遍歷,就必須正確地實現 Iterable 接口

  實際上,這種作法就是23中設計模式中的迭代器模式

三、那麼又有一個問題:數組並無實現 Iterable 接口啊,爲何數組也能夠用 foreach 循環遍歷呢?

  其實:Java 將對於數組的 foreach 循環轉換爲對於這個數組每個的循環引用。

  因此結論爲:編譯器對集合的 forEach 會調用集合的 迭代器;對數組的 forEach 會調用數組的 for 循環

  測試:java文件

  編譯後:class 文件

 

  

 參考:https://blog.csdn.net/zymx14/article/details/78394464

相關文章
相關標籤/搜索