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...