Java,你告訴我 fail-fast 是什麼鬼?

0一、前言

提及來真特麼慚愧:十年 IT 老兵,Java 菜鳥一枚。今天我才瞭解到 Java 還有 fail-fast 一說。不得不感慨啊,學習真的是沒有止境。只要肯學,就會有巨多巨多別人眼中的「舊」知識涌現出來,而且在我這全是新的。java

能怎麼辦呢?除了羞愧,就只能趕忙全身心地投入學習,把這些知識掌握。程序員

爲了鎮樓,必須搬一段英文來解釋一下 fail-fast。bash

In systems design, a fail-fast system is one which immediately reports at its interface any condition that is likely to indicate a failure. Fail-fast systems are usually designed to stop normal operation rather than attempt to continue a possibly flawed process. Such designs often check the system's state at several points in an operation, so any failures can be detected early. The responsibility of a fail-fast module is detecting errors, then letting the next-highest level of the system handle them.多線程

你們不嫌棄的話,我就用蹩腳的英語能力翻譯一下。某場戰役當中,政委發現司令員在亂指揮的話,就立馬報告給權限更高的中央軍委——這樣能夠有效地避免更嚴重的後果出現。固然了,若是司令員是李雲龍的話,報告也沒啥用。併發

不過,Java 的世界裏不存在李雲龍。fail-fast 扮演的就是政委的角色,一旦報告給上級,後面的行動就別想執行。ide

怎麼和代碼關聯起來呢?看下面這段代碼。源碼分析

public void test(Wanger wanger) {	
	if (wanger == null) {
		throw new RuntimeException("wanger 不能爲空");
	}
	
	System.out.println(wanger.toString());
}
複製代碼

一旦檢測到 wanger 爲 null,就立馬拋出異常,讓調用者來決定這種狀況下該怎麼處理,下一步 wanger.toString() 就不會執行了——避免更嚴重的錯誤出現,這段代碼因爲太過簡單,體現不出來,後面會講到。學習

瞧,fail-fast 就是這個鬼,沒什麼神祕的。若是你們源碼看得比較多的話,這種例子多得就像旅遊高峯期的人頭。ui

而後呢,沒了?三秒鐘,彆着急,咱們繼續。this

0二、for each 中集合的 remove 操做

很長一段時間裏,都不明白爲何不能在 for each 循環裏進行元素的 remove。今天咱們就來藉機來體驗一把。

List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一個文章真特麼有趣的程序員");

for (String str : list) {
	if ("沉默王二".equals(str)) {
		list.remove(str);
	}
}

System.out.println(list);
複製代碼

這段代碼看起來沒有任何問題,但運行起來就糟糕了。

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at com.cmower.java_demo.str.Cmower3.main(Cmower3.java:14)
複製代碼

爲毛呢?

0三、分析問題的殺手鐗

這時候就只能看源碼了,ArrayList.java 的 909 行代碼是這樣的。

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

也就是說,remove 的時候執行了 checkForComodification 方法,該方法對 modCount 和 expectedModCount 進行了比較,發現二者不等,就拋出了 ConcurrentModificationException 異常。

可爲何會執行 checkForComodification 方法呢?這就須要反編譯一下 for each 那段代碼了。

List<String> list = new ArrayList();
list.add("沉默王二");
list.add("沉默王三");
list.add("一個文章真特麼有趣的程序員");
Iterator var3 = list.iterator();

while (var3.hasNext()) {
	String str = (String) var3.next();
	if ("沉默王二".equals(str)) {
		list.remove(str);
	}
}

System.out.println(list);
複製代碼

原來 for each 是經過迭代器 Iterator 配合 while 循環實現的。

1)ArrayList.iterator() 返回的 Iterator 實際上是 ArrayList 的一個內部類 Itr。

public Iterator<E> iterator() {
    return new Itr();
}
複製代碼

Itr 實現了 Iterator 接口。

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
    int expectedModCount = modCount;

    Itr() {}

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

    @SuppressWarnings("unchecked")
    public E next() {
        checkForComodification();
        int i = cursor;
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }
}
複製代碼

也就是說 new Itr() 的時候 expectedModCount 被賦值爲 modCount,而 modCount 是 List 的一個成員變量,表示集合被修改的次數。因爲 list 此前執行了 3 次 add 方法,因此 modCount 的值爲 3;expectedModCount 的值也爲 3。

可當執行 list.remove(str) 後,modCount 的值變成了 4。

private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work
}
複製代碼

注:remove 方法內部調用了 fastRemove 方法。

下一次循環執行到 String str = (String) var3.next(); 的時候,就會調用 checkForComodification 方法,此時一個爲 3,一個爲 4,就只好拋出異常 ConcurrentModificationException 了。

不信,能夠直接在 ArrayList 類的 909 行打個斷點 debug 一下。

真的耶,一個是 4 一個是 3。

總結一下。在 for each 循環中,集合遍歷實際上是經過迭代器 Iterator 配合 while 循環實現的,可是元素的 remove 卻直接使用的集合類自身的方法。這就致使 Iterator 在遍歷的時候,會發現元素在本身不知情的狀況下被修改了,它以爲很難接受,就拋出了異常。

讀者朋友們,大家是否是以爲我跑題了,fail-fast 和 for each 中集合的 remove 操做有什麼關係呢?

有!Iterator 使用了 fail-fast 的保護機制。

0四、怎麼避開 fail-fast 保護機制呢

經過上面的分析,相信你們都明白爲何不能在 for each 循環裏進行元素的 remove 了。

那怎麼避開 fail-fast 保護機制呢?畢竟刪除元素是常規操做,咱不能因噎廢食啊。

1)remove 後 break

List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一個文章真特麼有趣的程序員");

for (String str : list) {
	if ("沉默王二".equals(str)) {
		list.remove(str);
		break;
	}
}
複製代碼

我怎麼這麼聰明,忍不住驕傲一下。有讀者不明白爲何嗎?那我上面的源碼分析可就白分析了,爬樓再看一遍吧!

略微透露一下緣由:break 後循環就再也不遍歷了,意味着 Iterator 的 next 方法再也不執行了,也就意味着 checkForComodification 方法再也不執行了,因此異常也就不會拋出了。

可是呢,當 List 中有重複元素要刪除的時候,break 就不合適了。

2)for 循環

List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一個文章真特麼有趣的程序員");
for (int i = 0, n = list.size(); i < n; i++) {
	String str = list.get(i);
	if ("沉默王二".equals(str)) {
		list.remove(str);
	}
}
複製代碼

for 循環雖然能夠避開 fail-fast 保護機制,也就說 remove 元素後再也不拋出異常;可是呢,這段程序在原則上是有問題的。爲何呢?

第一次循環的時候,i 爲 0,list.size() 爲 3,當執行完 remove 方法後,i 爲 1,list.size() 卻變成了 2,由於 list 的大小在 remove 後發生了變化,也就意味着「沉默王三」這個元素被跳過了。能明白嗎?

remove 以前 list.get(1) 爲「沉默王三」;但 remove 以後 list.get(1) 變成了「一個文章真特麼有趣的程序員」,而 list.get(0) 變成了「沉默王三」。

3)Iterator

List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一個文章真特麼有趣的程序員");

Iterator<String> itr = list.iterator();

while (itr.hasNext()) {
	String str = itr.next();
	if ("沉默王二".equals(str)) {
		itr.remove();
	}
}
複製代碼

爲何使用 Iterator 的 remove 方法就能夠避開 fail-fast 保護機制呢?看一下 remove 的源碼就明白了。

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();
    }
}
複製代碼

雖然刪除元素依然使用的是 ArrayList 的 remove 方法,可是刪除完會執行 expectedModCount = modCount,保證了 expectedModCount 與 modCount 的同步。

0五、最後

在 Java 中,fail-fast 從狹義上講是針對多線程狀況下的集合迭代器而言的。這一點能夠從 ConcurrentModificationException 定義上看得出來。

This exception may be thrown by methods that have detected concurrent modification of an object when such modification is not permissible.
For example, it is not generally permissible for one thread to modify a Collectionwhile another thread is iterating over it. In general, the results of theiteration are undefined under these circumstances. Some Iteratorimplementations (including those of all the general purpose collection implementationsprovided by the JRE) may choose to throw this exception if this behavior isdetected. Iterators that do this are known as fail-fast iterators,as they fail quickly and cleanly, rather that risking arbitrary,non-deterministic behavior at an undetermined time in the future.

再次拙劣地翻譯一下。

該異常可能因爲檢測到對象在併發狀況下被修改而拋出的,而這種修改是不容許的。

一般,這種操做是不容許的,好比說一個線程在修改集合,而另外一個線程在迭代它。這種狀況下,迭代的結果是不肯定的。若是檢測到這種行爲,一些 Iterator(好比說 ArrayList 的內部類 Itr)就會選擇拋出該異常。這樣的迭代器被稱爲 fail-fast 迭代器,由於儘早的失敗比將來出現不肯定的風險更好。

既然是針對多線程,爲何咱們以前的分析都是基於單線程的呢?由於從廣義上講,fail-fast 指的是當有異常或者錯誤發生時就當即中斷執行的這種設計,從單線程的角度去分析,你們更容易明白。

你說對嗎?

0六、致謝

謝謝你們的閱讀,原創不易,喜歡就隨手點個贊👍,這將是我最強的寫做動力。若是以爲文章對你有點幫助,還挺有趣,就關注一下個人公衆號「沉默王二」;回覆「666」更有 500G 高質量教學視頻相送(已分門別類)。

相關文章
相關標籤/搜索