快速失敗(fail-fast)與安全失敗(fail-safe)

fail-fast與fail-safe

在Collection集合的各個類中,有線程安全和線程不安全這2大類的版本。

對於線程不安全的類,併發情況下可能會出現fail-fast情況;而線程安全的類,可能出現fail-safe的情況。

一、併發修改

當一個或多個線程正在遍歷一個集合Collection的時候(Iterator遍歷),而此時另一個線程修改了這個集合的內容(如添加,刪除或者修改)。這就是併發修改的情況。

二、fail-fast快速失敗

fail-fast機制:當遍歷一個集合對象時,如果集合對象的結構被修改了,就會拋出ConcurrentModificationExcetion異常。

有2種情況會拋出該異常:

  1. 在單線程的情況下,如果使用Iterator對象遍歷集合對象的過程中,修改了集合對象的結構。如下:

    // 1.iterator迭代,拋出ConcurrentModificationException異常
    Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()) {
      String s = iterator.next();
      System.out.println(s);
      // 修改集合結構
      if ("s2".equals(s)) {
        list.remove(s);
      }
    }
    
    // 2.foreach迭代,拋出ConcurrentModificationException異常
    for (String s : list) {
      System.out.println(s);
      // 修改集合結構
      if ("s2".equals(s)) {
        list.remove(s);
      }
    }

    要想避免拋出異常,應該使用Iterator對象的remove()方法。

    // 3.iterator迭代,使用iterator.remove()移除元素不會拋出異常
    Iterator<String> iterator2 = list.iterator();
    while (iterator2.hasNext()) {
      String s = iterator2.next();
      System.out.println(s);
      // 修改集合結構
      if ("s2".equals(s)) {
      iterator2.remove();
      }
    }
  2. 在多線程環境下,如果對集合對象進行併發修改,那麼就會拋出ConcurrentModificationException異常。

注意,迭代器的快速失敗行爲無法得到保證,因爲一般來說,不可能對是否出現不同步併發修改做出任何硬性保證。快速失敗迭代器會盡最大努力拋出 ConcurrentModificationException。因此,爲提高這類迭代器的正確性而編寫一個依賴於此異常的程序是錯誤的做法,迭代器的快速失敗行爲應該僅用於檢測 bug。

以ArrayList爲例,講解一下fail-fast的機制

1、單線程下,使用iterator迭代時的情況

ArrayList繼承自AbstractList類,AbstractList內部有一個字段modCount,代表修改的次數。

fail-1

ArrayList類的add、remove操作都會使得modCount自增。

fail-2

fail-3

當使用ArrayList.iterator()返回一個迭代器對象時。迭代器對象有一個屬性expectedModCount,它被賦值爲該方法調用時modCount的值。這意味着,這個值是modCount在這個時間點的快照值,expectedModCount值在iterator對象內部不會再發送變化!

fail-4

這時候我們就能明白了,在得到迭代器之後,如果我們使用ArrayList的add、remove等方法,會使得modCount的值自增(發生了變化),而iterator內部的expectedModCount值卻還是之前的快照值。我們再來看iterator的方法實現:可以看到,在調用next方法時,第一步就是檢查modCount值和迭代器內部的expectedModCount值是否相等!顯然,這是不等的,所以在調用next方法的時候,就拋出了ConcurrentModificationException異常。

fail-5

fail-6

爲什麼說迭代器的fail-fast機制是盡最大努力地拋出ConcurrentModificationException異常呢?

原因就是上面我們看到的,只有在迭代過程中修改了元素的結構,當再調用next()方法時纔會拋出該異常。也就是說,如果迭代過程中發生了修改,但之後沒有調用next()迭代,該異常就不會拋出了!(該異常的機制是告訴你,當前迭代器要進行操作是有問題的,因爲集合對象現在的狀態發生了改變!)

那爲什麼iterator.remove()方法可行呢?

下圖中,可以看到,remove方法沒有進行modCount值的檢查,並且手動把expectedModCount值修改成了modCount值,這又保證了下一次迭代的正確。

fail-7

2、多線程下的情況

當然,如果多線程下使用迭代器也會拋出ConcurrentModificationException異常。而如果不進行迭代遍歷,而是併發修改集合類,則可能會出現其他的異常如數組越界異常。

三、fail-safe安全失敗

Fail-Safe 迭代的出現,是爲了解決fail-fast拋出異常處理不方便的情況。fail-safe是針對線程安全的集合類。

上面的fail-fast發生時,程序會拋出異常,而fail-safe是一個概念,併發容器的併發修改不會拋出異常,這和其實現有關。併發容器的iterate方法返回的iterator對象,內部都是保存了該集合對象的一個快照副本,並且沒有modCount等數值做檢查。如下圖,這也造成了併發容器的iterator讀取的數據是某個時間點的快照版本。你可以併發讀取,不會拋出異常,但是不保證你遍歷讀取的值和當前集合對象的狀態是一致的!這就是安全失敗的含義。

fail-8

所以Fail-Safe 迭代的缺點是:首先是iterator不能保證返回集合更新後的數據,因爲其工作在集合克隆上,而非集合本身。其次,創建集合拷貝需要相應的開銷,包括時間和內存。

在java.util.concurrent 包中集合的迭代器,如 ConcurrentHashMap, CopyOnWriteArrayList等默認爲都是Fail-Safe。

// 1.foreach迭代,fail-safe,不會拋出異常
for (String s : list) {
  System.out.println(s);
  if ("s1".equals(s)) {
  list.remove(s);
  }
}

// 2.iterator迭代,fail-safe,不會拋出異常
Iterator<String> iterator = list.iterator();
  while (iterator.hasNext()) {
  String s = iterator.next();
  System.out.println(s);
  if ("s1".equals(s)) {
  list.remove(s);
  }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章