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

fail-fast和fail-safe的區別: 
fail-safe容許在遍歷的過程當中對容器中的數據進行修改,而fail-fast則不容許。java

fail-fast ( 快速失敗 )
fail-fast:直接在容器上進行遍歷,在遍歷過程當中,一旦發現容器中的數據被修改了,會馬上拋出ConcurrentModificationException異常致使遍歷失敗。java.util包下的集合類都是快速失敗機制的, 常見的的使用fail-fast方式遍歷的容器有HashMap和ArrayList等。安全

在使用迭代器遍歷一個集合對象時,好比加強for,若是遍歷過程當中對集合對象的內容進行了修改(增刪改),會拋出ConcurrentModificationException 異常.多線程

fail-fast的出現場景
在咱們常見的java集合中就可能出現fail-fast機制,好比ArrayList,HashMap。在多線程和單線程環境下都有可能出現快速失敗。
一、單線程環境下的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。ide

HashMap發生fail-fast:ui

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異常:this

二、多線程環境下:.net

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,但一樣地,迭代器並不能訪問到修改後的內容,即:迭代器遍歷的是開始遍歷那一刻拿到的集合拷貝,在遍歷期間原集合發生的修改迭代器是不知道的。
————————————————
版權聲明:本文爲CSDN博主「striner」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處連接及本聲明。
原文連接:https://blog.csdn.net/striner...

相關文章
相關標籤/搜索