java基礎提升之 fail-fast

        在查看集合類源碼時,fail-fast這個詞出現的頻率很高,幾乎每個集合類中都會出現,好比ArrayList、HashMap、HashSet、LinkedHashMap等。這一篇文章咱們未來討論一下什麼是fail-fast以及fail-fast的實現原理。java

什麼是fail-fast

        fail-fast(快速失敗機制)是java集合中的一種錯誤機制。在HashMap中有這麼一段描述fail-fast的機制的註釋文字:併發

The iterators returned by all of this class's "collection view methods" are fail-fast: if the map is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove method, the iterator will throw a {@link ConcurrentModificationException}. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future. 函數

        上面這段英文告訴了咱們fail-fast是如何出現的,其中提到當一個迭代器被建立(這裏咱們單純的認爲是調用了HashMap的keySet()、values()等方法後根據返回的結果又調用了iterator方法)後,若是map的結構被修改(此時的結構修改是指map的size發生變化,好比擴容、新建或者刪除了其中的元素,更新不算結構修改)。那麼在返回的iterator中進行遍歷會拋出一個 ConcurrentModificationException錯誤。這就是快速失敗機制。所以,在併發修改的狀況下,迭代器快速而乾淨的失敗,而不是在將來的未肯定時間冒着任意的非肯定行爲的風險。ui

        對於爲什麼對原集合類操做會影響到以前返回的iterator咱們會在下面進行說明。this

        另外在HashMap解釋完fail-fast後又緊跟着來了這麼一段:spa

Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs. code

        這段的大體意思是,迭代器的fail-fast機制並不能被保證必定會發生,通常來講,在存在不一樣步的併發修改時不可能作出任何有力保證,可是fail-fast會盡最大努力拋出ConcurrentModificationException錯誤。所以若是一個程序依賴這個異常去保證其運行的正確性是錯誤的,快速失敗機制只能用來標識這個錯誤。cdn

        在源碼中就好比HashMap的keySet()方法,在註釋中明確提出了,這個方法返回的集合由HashMap支持,因此任何對map的更改的結果都會反映到返回的set上,反之也同樣。咱們經過代碼來驗證這一段話blog

package com.lichaobao.collectionsown.collections;

import java.util.*;
import java.util.stream.Stream;

/** * @author lichaobao * @date 2019/5/11 * @QQ 1527563274 */
public class MapTestMain {
    public static void main(String[] args){
        Map<String, String> map = new HashMap<>();
        String b = map.put("2","2Value");
        String c = map.put("3","3Value");
        String a = map.put("1","1Value");
        String e = map.put("5","5Value");
        String f = map.put("6","6Value");
        String d = map.put("4","4Value");
        Set<String> set = map.keySet();
        System.out.println("初始");
        System.out.println(map.toString());
        System.out.println(set.toString());
        System.out.println("===============");
        System.out.println("set刪除1");
        set.remove("1");
        System.out.println(map.toString());
        System.out.println(set.toString());
        System.out.println("===============");
        System.out.println("map刪除3");
        map.remove("3");
        System.out.println(map.toString());
        System.out.println(set.toString());
        System.out.println("===============");
    }
}

複製代碼

運行後返回結果以下:rem

咱們能夠發現不論是對set操做仍是對源map進行操做,最後結果都會反映到對方身上。

        那麼爲何會這樣呢,咱們來看一下在HashMap中的KeySet方法:

        咱們發現返回的實際上是HashMap中的KeySet內部類咱們再來看一下HashMap類中KeySet類:

        發現啥沒?沒錯,其實全部的KeySet的操做都是調用HashMap的方法來完成的,這就是爲何對返回的KeySet中的操做會反映到HashMap以及對HashMap進行操做也會反映到KeySet的緣由。在我我的認爲這也是fail-fast機制出現的根本緣由。

fail-fast 實現原理

        咱們經過HashMap來討論fail-fast機制。查看HashMap咱們能夠發現HashMap中有這麼一個變量:

        就是這個modCount協助完成了fail-fast機制。關於modCount咱們能夠看到註釋中是這樣寫的:記錄了這個HashMap結構修改的次數。結構修改指的是更改HashMap中的鍵值對的數量以及其餘方式修改器內部結構(如rehash)的修改。這個字段被用來在HashMap的迭代器上作快速失敗。你們能夠在HashMap的源碼中查看什麼時刻modCount被修改了,去驗證這一句話。

只有一個modCount還不足以完成fail-fast。咱們上文說到,快速失敗是在Iterator迭代中產生的。那咱們看看在HashMap中內置的Iterator類:

abstract class HashIterator {
        Node<K,V> next;        // next entry to return
        Node<K,V> current;     // current entry
        int expectedModCount;  // for fast-fail
        int index;             // current slot

        HashIterator() {
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            if (t != null && size > 0) { // advance to first entry
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }

        public final boolean hasNext() {
            return next != null;
        }

        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }

        public final void remove() {
            Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }
複製代碼

        沒錯就是這個抽象類 HashIterator。keySet()、values()、entrySet()方法都是依靠這個類來完成的。如今咱們把關注點放在這個抽象類的一個字段上:

        就是紅圈圈住的expectedModCount 源碼再次很貼心得告訴了咱們這個字段用來作fast-fail,那具體這個字段是怎麼用的呢? 咱們來看HashIterator的構造函數:

        咱們發如今Iterator生成的時候這個expectedModCount被賦上了modCount的值。好了,鋪墊了這麼多,咱們終於能夠說到重點,那就是fail-fast機制究竟是如何實現的,咱們把關注點放在HashIterator類中的nextNode()以及remove方法上:

其實如今就很清楚了,當Iterator類內部的expectedModCount!=modCount時就會拋出ConcurrentModificationException。咱們如今用代碼來演示一下fail-fast:

package com.lichaobao.collectionsown.collections;

import java.util.*;
import java.util.stream.Stream;

/** * @author lichaobao * @date 2019/5/11 * @QQ 1527563274 */
public class MapTestMain {
    public static void main(String[] args){
        Map<String, String> map = new HashMap<>();
        String b = map.put("2","2Value");
        String c = map.put("3","3Value");
        String a = map.put("1","1Value");
        String e = map.put("5","5Value");
        String f = map.put("6","6Value");
        String d = map.put("4","4Value");
        Set<String> set = map.keySet();
        Iterator<String> iterable = set.iterator();
        System.out.println("初始");
        System.out.println(map.toString());
        System.out.println(set.toString());
        System.out.println("===============");
        System.out.println("set刪除1");
        set.remove("1");
        System.out.println(map.toString());
        System.out.println(set.toString());
        System.out.println("===============");
        while (iterable.hasNext()){
            System.out.println(iterable.next());
        }
    }
}

複製代碼

        如今咱們來看一下運行結果:

        本篇結束!

相關文章
相關標籤/搜索