fail fast 機制是Java集合(Collection)中的一種錯誤機制。當多個線程對同一個集合的內容進行操作時,就可能會產生fail fast(快速失敗)事件。例如:當某一個線程A通過iterator去遍歷某集合的過程中,若該集合的內容被其他線程所改變了;那麼線程A訪問集合時,就會拋出Con ...
fail-fast 機制是Java集合(Collection)中的一種錯誤機制。當多個線程對同一個集合的內容進行操作時,就可能會產生fail-fast(快速失敗)事件。例如:當某一個線程A通過iterator去遍歷某集合的過程中,若該集合的內容被其他線程所改變了;那麼線程A訪問集合時,就會拋出ConcurrentModificationException異常,產生fail-fast事件。
迭代器的快速失敗行為無法得到保證,它不能保證一定會出現該錯誤,但是快速失敗操作會盡最大努力拋出ConcurrentModificationException異常。
註意:上面所說的是在多線程環境下會發生fail-fast事件,但是單線程條件下如果違反了規則也是會產生fail-fast事件的
在文檔中有這麼一段話:編寫的程式依賴於快速失敗機制產生的異常是不對的,迭代器的快速檢測機制僅僅用於檢測錯誤。
分別用兩段程式測試快速失敗機制產生的原因:
單線程環境:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Created with IDEA
*
* @author DuzhenTong
* @Date 2018/3/18
* @Time 17:34
*/
public class FailFast {
public static void main(String[] args) {
List list = new ArrayList();
for (int i = 0; i < 10; i++) {
list.add(i);
}
iterator(list);
}
public static void iterator(List list) {
Iterator it = list.iterator();
int index = 0;
while (it.hasNext()) {
if (index == 6) {
list.remove(index);
}
index++;
System.out.println(it.next());
}
}
}
輸出結果:
0
1
2
3
4
5
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:819)
at java.util.ArrayList$Itr.next(ArrayList.java:791)
at FailFast.iterator(FailFast.java:29)
at FailFast.main(FailFast.java:18)
多線程環境:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Created with IDEA
*
* @author DuzhenTong
* @Date 2018/3/18
* @Time 17:59
*/
public class FailFast1 {
public static List list = new ArrayList();
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
list.add(i);
}
new ThreadA().start();
new ThreadB().start();
}
public static class ThreadA extends Thread {
@Override
public void run() {
Iterator it = list.iterator();
while (it.hasNext()) {
System.out.println("集合遍歷中……:"+it.next());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static class ThreadB extends Thread {
@Override
public void run() {
int index = 0;
while (index != 10) {
System.out.println("線程等待中……:"+index);
if (index == 3) {
list.remove(index);
}
index++;
}
}
}
}
輸出結果:
線程等待中……:0
集合遍歷中……:0
線程等待中……:1
線程等待中……:2
線程等待中……:3
線程等待中……:4
線程等待中……:5
線程等待中……:6
線程等待中……:7
線程等待中……:8
線程等待中……:9
Exception in thread "Thread-0" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:819)
at java.util.ArrayList$Itr.next(ArrayList.java:791)
at FailFast1$ThreadA.run(FailFast1.java:28)
上面的程式已經說明瞭為什麼會發生fail-fast事件(快速失敗),在多線程條件下,一個線程正在遍歷集合中的元素,這時候另一個線程更改了集合的結構,程式才會拋出ConcurrentModificationException,在單線程條件下也是在遍歷的時候,這時候更改集合的結構,程式就會拋出ConcurrentModificationException。
要具體知道為什麼會出現fail-fast,就要分析源碼,fail-fast出現是在遍歷集合的時候出現的,也就是對集合進行迭代的時候,對集合進行迭代的時候都是操作迭代器,集合中的內部類:(ArrayList源碼)
private class Itr implements Iterator<E> {
int cursor;
int lastRet = -1;
int expectedModCount = modCount;//---------------------1
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
//………此處代碼省略…………
}
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();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
上面的代碼中我們可以看到拋出ConcurrentModificationException異常,上面的next(),remove()都會調用checkForComodification()方法檢查兩個變數的值是否相等,不相等就會拋出異常。在上面程式中的數字1處:
int expectedModCount = modCount;
modCount的值賦值給expectedModCount,知道modCount這個值的含義是什麼?為什麼會發生改變?原因就會找到了。
源碼點進去,發現這個modCount變數並不在ArrayList類中,而在AbstractList中,作為一個成員變數。
protected transient int modCount = 0;
接下來分析源碼,看最常用的方法
ArrayList中add方法:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
調用的ensureCapacityInternal方法:
private void ensureCapacityInternal(int minCapacity) {
modCount++;//modCount自增————————————
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
ArrayList中的remove方法:
public E remove(int index) {
rangeCheck(index);
modCount++;//modCount自增——————————————
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // Let gc do its work
return oldValue;
}
ArrayList中的clear方法:
public void clear() {
modCount++;//modCount自增——————————————
// Let gc do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
分析了源碼就知道原因是什麼了,凡是涉及到改變了集合的結構(改變元素的個數)的操作(包括增加,移除或者清空等)modCount這個變數都會自增,在獲得迭代對象的時候,先把這個modCount變數賦值給expectedModCount,在迭代的時候每次都會檢查這個變數是否與expectedModCount一致,因為如果是在集合中添加或者刪除元素modCount的值都會發生改變。
解決方法:
- 對於涉及到更改集合中元素個數的操作通通加上synchronized,或者利用Collections.synchronizedList強制他們的操作都是同步的。
- 使用CopyOnWriteArrayList來替換ArrayList
這裡在網上看到很多的文章都是這麼說的:為什麼CopyOnWriteArrayList可以做到不會發生fail-fast?因為CopyOnWriteArrayList所有可變操作(add、set 等等)都是通過對底層數組進行一次新的複製來實現的。
可以分析源碼(下麵的1,2處)
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();//————————1
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);//————————2
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
我的看法:原因不止有CopyOnWriteArrayList的add、set、remove等會改變原數組的方法中,都是先copy一份原來的array,再在copy數組上進行add、set、remove操作,這就才不影響COWIterator那份數組。
為什麼沒有記錄修改次數的值或者說不比較modCount也能做到記憶體的一致性呢?
在上面的代碼1處,調用了getArray()方法,看源碼:
final Object[] getArray() {
return array;
}
private volatile transient Object[] array;
因為getArray()返回的array的類型是用volatile修飾的,volatile類型的(強制記憶體一致性)
具體可以看我的另一篇關於volatile的:volatile關鍵字解析
參考文章:http://cmsblogs.com/?p=1220