1. 程式人生 > >fail-fast(快速失敗)機制和fail-safe(安全失敗)機制的介紹和區別

fail-fast(快速失敗)機制和fail-safe(安全失敗)機制的介紹和區別

fail-fast和fail-safe的區別: 

fail-safe允許在遍歷的過程中對容器中的資料進行修改,而fail-fast則不允許。


fail-fast ( 快速失敗 )

fail-fast:直接在容器上進行遍歷,在遍歷過程中,一旦發現容器中的資料被修改了,會立刻丟擲ConcurrentModificationException異常導致遍歷失敗。java.util包下的集合類都是快速失敗機制的, 常見的的使用fail-fast方式遍歷的容器有HashMap和ArrayList等。

在使用迭代器遍歷一個集合物件時,比如增強for,如果遍歷過程中對集合物件的內容進行了修改(增刪改),會丟擲ConcurrentModificationException 異常.

fail-fast的出現場景
在我們常見的java集合中就可能出現fail-fast機制,比如ArrayList,HashMap。在多執行緒和單執行緒環境下都有可能出現快速失敗。
1、單執行緒環境下的fail-fast:
ArrayList發生fail-fast例子:

public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    for (int i = 0 ; i < 10 ; i++ ) {
        list.add(i + "");
    }
    Iterator<String> iterator = list.iterator();
    int i = 0 ;
    while(iterator.hasNext()) {
        if (i == 3) {
             list.remove(3);
        }
        System.out.println(iterator.next());
        i ++;
    }
} 

該段程式碼定義了一個Arraylist集合,並使用迭代器遍歷,在遍歷過程中,刻意在某一步迭代中remove一個元素,這個時候,就會發生fail-fast。

HashMap發生fail-fast:

public static void main(String[] args) {
    Map<String, String> map = new HashMap<>();
    for (int i = 0 ; i < 10 ; i ++ ) {
        map.put(i+"", i+"");
    }
    Iterator<Entry<String, String>> it = map.entrySet().iterator();
    int i = 0;
    while (it.hasNext()) {
       if (i == 3) {
           map.remove(3+"");
       }
       Entry<String, String> entry = it.next();
       System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
          i++;
    }
}

該段程式碼定義了一個hashmap物件並存放了10個鍵值對,在迭代遍歷過程中,使用map的remove方法移除了一個元素,導致丟擲了ConcurrentModificationException異常:

2、多執行緒環境下:

public class FailFastTest {
     public static List<String> list = new ArrayList<>();
 
     private static class MyThread1 extends Thread {
           @Override
           public void run() {
                Iterator<String> iterator = list.iterator();
                while(iterator.hasNext()) {
                     String s = iterator.next();
                     System.out.println(this.getName() + ":" + s);
                     try {
                       Thread.sleep(1000);
                     } catch (InterruptedException e) {
                        e.printStackTrace();
                     }
                }
                super.run();
           }
     }
 
     private static class MyThread2 extends Thread {
           int i = 0;
           @Override
           public void run() {
                while (i < 10) {
                     System.out.println("thread2:" + i);
                     if (i == 2) {
                         list.remove(i);
                     }
                     try {
                         Thread.sleep(1000);
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                     i ++;
                }
           }
     }
 
     public static void main(String[] args) {
           for(int i = 0 ; i < 10;i++){
               list.add(i+"");
           }
           MyThread1 thread1 = new MyThread1();
           MyThread2 thread2 = new MyThread2();
           thread1.setName("thread1");
           thread2.setName("thread2");
           thread1.start();
           thread2.start();
     }
}

啟動兩個執行緒,分別對其中一個對list進行迭代,另一個線上程1的迭代過程中去remove一個元素,結果也是丟擲了java.util.ConcurrentModificationException

fail-fast的原理:

fail-fast是如何丟擲ConcurrentModificationException異常的,又是在什麼情況下才會丟擲?
我們知道,對於集合如list,map類,我們都可以通過迭代器來遍歷,而Iterator其實只是一個介面,具體的實現還是要看具體的集合類中的內部類去實現Iterator並實現相關方法。這裡我們就以ArrayList類為例。在ArrayList中,當呼叫list.iterator()時,其原始碼是: 

public Iterator<E> iterator() {
        return new Itr();
}

即它會返回一個新的Itr類,而Itr類是ArrayList的內部類,實現了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
        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();
        }
    }

其中,有三個屬性:

int cursor;       // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;

cursor是指集合遍歷過程中的即將遍歷的元素的索引,lastRet是cursor -1,預設為-1,即不存在上一個時,為-1,它主要用於記錄剛剛遍歷過的元素的索引。expectedModCount這個就是fail-fast判斷的關鍵變量了,它初始值就為ArrayList中的modCount。(modCount是抽象類AbstractList中的變數,預設為0,而ArrayList 繼承了AbstractList ,所以也有這個變數,modCount用於記錄集合操作過程中作的修改次數,與size還是有區別的,並不一定等於size)
我們一步一步來看:

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

迭代器迭代結束的標誌就是hasNext()返回false,而該方法就是用cursor遊標和size(集合中的元素數目)進行對比,當cursor等於size時,表示已經遍歷完成。
接下來看看最關心的next()方法,看看為什麼在迭代過程中,如果有執行緒對集合結構做出改變,就會發生fail-fast:

@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];
}

從原始碼知道,每次呼叫next()方法,在實際訪問元素前,都會呼叫checkForComodification方法,該方法原始碼如下:

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

可以看出,該方法才是判斷是否丟擲ConcurrentModificationException異常的關鍵。在該段程式碼中,當modCount != expectedModCount時,就會丟擲該異常。但是在一開始的時候,expectedModCount初始值預設等於modCount,為什麼會出現modCount != expectedModCount,很明顯expectedModCount在整個迭代過程除了一開始賦予初始值modCount外,並沒有再發生改變,所以可能發生改變的就只有modCount,在前面關於ArrayList擴容機制的分析中,可以知道在ArrayList進行add,remove,clear等涉及到修改集合中的元素個數的操作時,modCount就會發生改變(modCount ++),所以當另一個執行緒(併發修改)或者同一個執行緒遍歷過程中,呼叫相關方法使集合的個數發生改變,就會使modCount發生變化,這樣在checkForComodification方法中就會丟擲ConcurrentModificationException異常。
類似的,hashMap中發生的原理也是一樣的。

避免fail-fast的方法:
瞭解了fail-fast機制的產生原理,接下來就看看如何解決fail-fast
方法1
在單執行緒的遍歷過程中,如果要進行remove操作,可以呼叫迭代器的remove方法而不是集合類的remove方法。看看ArrayList中迭代器的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();
   }
}

  可以看到,該remove方法並不會修改modCount的值,並且不會對後面的遍歷造成影響,因為該方法remove不能指定元素,只能remove當前遍歷過的那個元素,所以呼叫該方法並不會發生fail-fast現象。該方法有侷限性。

例子:

public static void main(String[] args) {
   List<String> list = new ArrayList<>();
   for (int i = 0 ; i < 10 ; i++ ) {
       list.add(i + "");
   }
   Iterator<String> iterator = list.iterator();
   int i = 0 ;
   while(iterator.hasNext()) {
       if (i == 3) {
           iterator.remove(); //迭代器的remove()方法
       }
       System.out.println(iterator.next());
       i ++;
   }
}

方法2

使用fail-safe機制,使用java併發包(java.util.concurrent)中的CopyOnWriterArrayList類來代替ArrayList,使用 ConcurrentHashMap來代替hashMap。


fail-safe ( 安全失敗 )

fail-safe:這種遍歷基於容器的一個克隆。因此,對容器內容的修改不影響遍歷。java.util.concurrent包下的容器都是安全失敗的,可以在多執行緒下併發使用,併發修改。常見的的使用fail-safe方式遍歷的容器有ConcerrentHashMap和CopyOnWriteArrayList等。

原理:

採用安全失敗機制的集合容器,在遍歷時不是直接在集合內容上訪問的,而是先複製原有集合內容,在拷貝的集合上進行遍歷。由於迭代時是對原集合的拷貝進行遍歷,所以在遍歷過程中對原集合所作的修改並不能被迭代器檢測到,所以不會觸發Concurrent Modification Exception。

缺點:基於拷貝內容的優點是避免了Concurrent Modification Exception,但同樣地,迭代器並不能訪問到修改後的內容,即:迭代器遍歷的是開始遍歷那一刻拿到的集合拷貝,在遍歷期間原集合發生的修改迭代器是不知道的。