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,但同樣地,迭代器並不能訪問到修改後的內容,即:迭代器遍歷的是開始遍歷那一刻拿到的集合拷貝,在遍歷期間原集合發生的修改迭代器是不知道的。