首頁 > 科技

fail-safe 和 fail-fast 都是什麼鬼?

2021-09-06 03:03:41

原文連結:https://mp.weixin.qq.com/s/dPA7zohM2LfBrZT-sfQnyg

你真的瞭解 fail-fast和 fail-safe嗎?

簡介

java.util 包下的 屬於 fail-fast , 快速失敗~

java.util.concurrent 包下的 屬於 fail-safe安全失敗~

簡單來說 就是 fail-fast 在迭代時,如果發現 該集合資料 結構被改變 (modCount != expectedModCount),就會 拋出 ConcurrentModificationException

小夥伴們可以參考下 下面的程式碼簡單實驗一下~

fail-fast實驗程式碼

實驗物件是 Hashtable,這裡採用 jdk1.7 的寫法 ~

因為博主還在研究 下文中 ConcurrentHashMap 在7和8中有啥不一樣

class E implements Runnable{    Hashtable<String, String> hashtable;    public E(Hashtable<String, String> hashtable) {        this.hashtable = hashtable;    }    private void add(Hashtable<String, String> hashtable){        for (int i = 0; i < 10000000; i++) {            hashtable.put("a",""+i);        }    }    @Override    public void run() {        add(hashtable);    }}public class D {    public static void main(String[] args) {        Hashtable<String, String> hashtable = new Hashtable<String, String>();        hashtable.put("1","2");        hashtable.put("2","2");        hashtable.put("3","2");        hashtable.put("4","2");        hashtable.put("15","2");        new Thread(new E(hashtable)).start();        Set<Map.Entry<String, String>> entries = hashtable.entrySet();        Iterator<Map.Entry<String, String>> iterator = entries.iterator();        try {            Thread.sleep(10);        } catch (InterruptedException e) {            e.printStackTrace();        }        while (iterator.hasNext()){            System.out.println(iterator.next());            iterator.remove();        }    }}

效果如圖:

觸發的原理:

當集合資料結構發生變化時,這兩個值是不相等的,所以會拋出該異常~ 。

結論:

雖然 HashTable 是 執行緒安全的 , 但是它有 fail-fast 機制 ,所以在多執行緒情況下進行 迭代 也不能去修改它的資料結構!

fail-fast 機制 不允許併發修改!

fail-safe實驗程式碼

class E implements Runnable{    ConcurrentHashMap<String, String> concurrentHashMap;    public E(ConcurrentHashMap<String, String> concurrentHashMap) {        this.concurrentHashMap = concurrentHashMap;    }    private void add( ConcurrentHashMap<String, String> concurrentHashMap){        for (int i = 0; i < 100000; i++) {            concurrentHashMap.put("a"+i,""+i);        }    }    @Override    public void run() {        add(concurrentHashMap);    }}public class D {    public static void main(String[] args) {        ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<String, String>();        concurrentHashMap.put("1","2");        concurrentHashMap.put("2","2");        concurrentHashMap.put("3","2");        concurrentHashMap.put("4","2");        concurrentHashMap.put("15","2");        new Thread(new E(concurrentHashMap)).start();        try {            Thread.sleep(2);        } catch (InterruptedException e) {            e.printStackTrace();        }        Set<Map.Entry<String, String>> entries = concurrentHashMap.entrySet();        for (Map.Entry<String, String> entry : entries) {            System.out.println(entry);//            這裡不用呼叫 iterator 去 remove            concurrentHashMap.remove(entry.getKey());        }    }}

效果如圖:

程式碼運行講解,執行緒A 往裡加資料,執行緒B 遍歷它的資料,並刪除。

可以看到這裡並沒有報錯~,但是它也不能保證遍歷到所有的值 (可以理解為無法獲取到最新的值)

有沒有感受到一絲絲 安全失敗的感覺~

哈哈哈 它的特點就是 允許併發修改,不會拋出 ConcurrentModificationException ,但是無法保證拿到的是最新的值

不知道小夥伴們看完上面的實驗程式碼有沒有疑惑

(((*)

為什麼可以呼叫它自身的 remove呢?

別急~ 我們先來看看使用這個迭代器中發生了什麼?

源碼走起~

小夥伴們可以看看下面四張圖~

創建迭代器的過程

圖一 可以看到會去創造一個 EntryIterator, 而 它又 繼承了 HashIterator ,在初始化時,會先呼叫父類的構造器。

圖三 可以發現 HashIterator 在初始化 時,會去呼叫 advance 方法 (這裡就不展開這個 concurrentHashMap結構啦~ ) 這裡的重點在最後一張圖 , 它呼叫的是 UNSAFE.getObjectVolatile

它的作用是 強制從主存中獲取屬性值。

小夥伴們可以自行對比下 HashMap 或者 上面的 HashTable,他們都是直接 拿到程式碼中定義的這個 Entry[]~。

不知道小夥伴們 get 得到這個點沒有~

哈哈哈 容我嘮叨嘮叨一下~

4ye 在網上搜這個 fail-fastfail-safe 的區別時,看到下面這張圖。

幾乎都在說 fail-safe 會複製原來的集合,然後在複製出來的集合上進行操作,然後就說這樣是不會拋出 ConcurrentModificationException 異常了。

可是這種說法是 不嚴謹的~ 它描述的情況應該是針對這個 CopyOnWriteArrayList 或者 CopyOnWriteArraySet 的情況(下面的源碼講到~)

CopyOnWriteArrayList 源碼

可以發現這裡 snapshot 的指針是始終指向這個原陣列的(當你創建迭代器的時候)

當你新增資料時,它會複製原來的陣列,並在複製出來的陣列上進行修改,

然後再設定進去,可以發現至始至終都沒有修改到這個原陣列,

所以迭代器中的資料是不受影響的~

結論

fail-safe 也是得具體情況具體分析的。

  1. 如果是 CopyOnWriteArrayList 或者 CopyOnWriteArraySet ,就屬於 複製原來的集合,然後在複製出來的集合上進行操作 的情況 ,所以是不會拋出這個 ConcurrentModificationException 的 。
  2. 如果是這個 concurrentHashMap 的,就比較硬核了~ 它直接操作底層,呼叫UNSAFE.getObjectVolatile ,直接 強制從主存中獲取屬性值,也是不會拋出這個 ConcurrentModificationException 的 。
  3. 併發下,無法保證 遍歷時拿到的是最新的值~

嘿嘿 現在回答上面那個 為啥可以 remove 的問題啦~

remove的源碼如下

重點在紅框處, pred 為 null 表示是陣列的頭部,此時呼叫 setEntryAt ,這裡也是出現了這個 UNSAFE

UNSAFE.putOrderedObject 這段程式碼的意思就是 :

有序的(有延遲的) 強制 更新資料到 主記憶體。(不能立刻被其他執行緒發現)

這些 和 Java 的 JMM (Java記憶體模型)有關!

總結

java.util 包下的屬於fail-fast

特點:

不允許併發修改,如果併發修改的話會導致在迭代過程中拋出 ConcurrentModificationException

出發點是 modCount != expectedModCount

java.util.concurrent 包下的 屬於 fail-safe

特點:

允許併發修改,但是 無法保證在迭代過程中獲取到最新的值 。

concurrentHashMap 獲取和修改資料時 ,是通過 UNSAFE 類 直接從主記憶體中獲取或者更新資料到主記憶體~

CopyOnWriteArrayList 或者 CopyOnWriteArraySet ,就直接 複製原來的集合,然後在複製出來的集合上進行操作


IT145.com E-mail:sddin#qq.com