原創/朱季謙
在Java編程當中,Iterator迭代器是一種用於遍歷如List、Set、Map等集合的工具。這類集合部分存在線程安全的問題,例如ArrayList,若在多線程環境下,迭代遍歷過程中存在其他線程對這類集合進行修改的話,就可能導致不一致或者修改異常問題,因此,針對這種情況,迭代器提供了兩種處理策略:Fail-Fast(快速失敗)和Fail-Safe(安全失敗)。
先簡單介紹下這兩種策略——
1. Fail-Fast(快速失敗)機制
快速失敗機制是指集合在迭代遍歷過程中,其他多線程或者當前線程對該集合進行增加或者刪除元素等操作,當前線程迭代器讀取集合時會立馬拋出一個ConcurrentModificationException異常,避免數據不一致。實現原理是迭代器在創建時,會獲取集合的計數變量當作一個標記,迭代過程中,若發現該標記大小與計數變量不一致了,就以爲集合做了新增或者刪除等操作,就會拋出快速失敗的異常。在ArrayList默認啓用該機制。
2. Fail-Safe(安全失敗)機制
安全失敗機制是指集合在迭代遍歷過程中,若其他多線程或者當前線程對該集合進行修改(增加、刪除等元素)操作,當前線程迭代器仍然可以正常繼續讀取集合遍歷,而不會拋出異常。該機制的實現,是通過迭代器在創建時,對集合進行了快照操作,即迭代器遍歷的是原集合的數組快照副本,若在這個過程,集合進行修改操作,會將原有的數組內容複製到新數組上,並在新數組上進行修改,修改完成後,再將集合數組的引用指向新數組,,而讀取操作仍然是訪問舊的快照副本,故而實現讀寫分離,保證讀取操作的線程安全性。在CopyOnWriteArrayList默認啓用該機制。
基於這兩個策略,分別寫一個案例來說明。
一、迭代器的Fail-Fast(快速失敗)機制原理
Fail-Fast(快速失敗)機制案例,用集合ArrayList來說明,這裏用一個線程就能模擬出該機制——
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("張三");
list.add("李四");
list.add("王五");
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
//第一次遍歷到這裏,能正常打印,第二次遍歷到這裏,因上一次遍歷做了list.add("李華")操作,集合已經改變,故而出現Fail-Fast(快速失敗)異常
String item = (String)iterator.next();
list.add("李華");
System.out.println(item);
}
System.out.println(list);
}
執行這段代碼,打印日誌出現異常ConcurrentModificationException,說明在遍歷過程當中,操作 list.add("李華")對集合做新增操作後,就會出現Fail-Fast(快速失敗)機制,拋出異常,阻止繼續進行遍歷——
張三
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
at java.util.ArrayList$Itr.next(ArrayList.java:861)
at ListExample.IteratorTest.main(IteratorTest.java:23)
這裏面是怎麼實現該Fail-Fast(快速失敗)機制的呢?
先來看案例裏創建迭代器的這行代碼Iterator iterator = list.iterator(),底層是這樣的——
public Iterator<E> iterator() {
return new Itr();
}
Itr類是ArrayList內部類,實現了Iterator
private class Itr implements Iterator<E> {
int cursor; // 迭代計數器
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
//判斷是否已經迭代到最後一位
public boolean hasNext() {
return cursor != size;
}
//取出當前遍歷到集合元素
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];
}
......
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
再進入案例裏的這行代碼 String item = (String)iterator.next()底層,也就是Itr類的public E next() {......}方法。
注意next()裏的這個方法 checkForComodification(),進入到方法裏,可以看到,ConcurrentModificationException異常正是在這個方法裏拋出來的,它做了一個判斷,判斷modCount是否等於expectedModCount,若不等於,就拋出快速失敗異常。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
那麼,問題就簡單了,研究ArrayList快速失敗機制,本質只需要看modCount和expectedModCount是什麼,就知道ArrayList的Fail-Fast(快速失敗)機制是怎麼處理的了。
在內部類Itr中,定義int expectedModCount = modCount,說明expectedModCount是在迭代器new Itr()創建時,就將此時的modCount數值賦值給變量expectedModCount,意味着,在整個迭代器生命週期內,這個expectedModCount是固定的了,從變量名就可以看出,它表示集合預期修改的次數,而modCount應該就是表示列表修改次數。假如迭代器創建時,modCount修改次數是5,那麼整個迭代器生命週期內,預期的修改次數expectedModCount就只能等於5。
請注意最爲關鍵的一個地方,modCount是可以變的。
先看一下在ArrayList裏,這個modCount是什麼?
這個modCount是定義在ArrayList的父類AbstractList裏的——
/**
*這個列表在結構上被修改的次數。結構修改是指改變列表,或者以其他方式擾亂它,使其迭代進步可能產生不正確的結果。
*
*該字段由迭代器和列表迭代器實現使用,由{@code迭代器}和{@code listtiterator}方法返回。
*如果該字段的值發生了意外變化,迭代器(或列表)將返回該字段迭代器)將拋出{@code ConcurrentModificationException}
*在響應{@code next}, {@code remove}, {@code previous},{@code set}或{@code add}操作。這提供了快速故障行爲。
*
*/
protected transient int modCount = 0;
根據註釋,可以得知,這是一個專門記錄列表被修改的次數,在ArrayList當中,涉及到add新增、remove刪除、fastRemove、clear等涉及列表結構改動的操作,,都會通過modCount++形式,增加列表在結構上被修改的次數。
modCount表示列表被修改的次數。
我們在案例代碼裏,做了add操作——
while(iterator.hasNext()) {
String item = (String)iterator.next();
list.add("李華");
System.out.println(item);
}
進入到ArrayList的add方法源碼裏,可以看到,在add新增過程中,按照ensureCapacityInternal =》ensureExplicitCapacity執行順序,最後通過modCount++修改了變量modCount——
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
總結一下,迭代器創建時,變量expectedModCount是被modCount賦值,在整個迭代器等生命週期中,變量expectedModCount值是固定的了,但在第一輪遍歷過程中,通過list.add("李華")操作,導致modCount++,最終就會出現expectedModCount != modCount。因此,在迭代器進行第二輪遍歷時,執行到 String item = (String)iterator.next(),在next()裏調用checkForComodification() 判斷expectedModCount是否還等於modCount,這時已經不等於,故而就會拋出ConcurrentModificationException異常,立刻結束迭代器遍歷,避免數據不一致。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
以上,就是集合迭代器的Fail-Fast機制原理。
二、迭代器的Fail-Safe(安全失敗)機制原理
Fail-Fast(快速失敗)機制案例,用集合CopyOnWriteArrayList來說明,這裏用一個線程就能模擬出該機制——
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("張三");
list.add("李四");
list.add("王五");
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
String item = (String)iterator.next();
list.add("李華");
System.out.println(item);
}
System.out.println("最後全部打印集合結果:" + list);
}
執行這段代碼,正常打印結果,說明在迭代器遍歷過程中,對集合做了新增元素操作,並不影響迭代器遍歷,新增的元素不會出現在迭代器遍歷當中,但是,在迭代器遍歷完成後,再一次打印集合,可以看到新增的元素已經在集合裏了——
張三
李四
王五
最後全部打印集合結果:[張三, 李四, 王五, 李華, 李華, 李華]
Fail-Safe(安全失敗)機制在CopyOnWriteArrayList體現,可以理解成,這是一種讀寫分離的機制。
下面就看一下CopyOnWriteArrayList是如何實現讀寫分離的。
先來看迭代器的創建Iterator iterator = list.iterator(),進入到list.iterator()底層源碼——
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
這裏的COWIterator是一個迭代器,關鍵有一個地方,在創建迭代器對象,調用其構造器時傳入兩個參數,分別是getArray()和0。
這裏的getArray()方法,獲取到一個array數組,它是CopyOnWriteArrayList集合真正存儲數據的地方。
final Object[] getArray() {
return array;
}
另一個參數0,表示迭代器遍歷的索引值,剛開始,肯定是從數組下標0開始。
明白getArray()和0這兩個參數後,看一下迭代器創建new COWIterator
static final class COWIterator<E> implements ListIterator<E> {
//列表快照
private final Object[] snapshot;
//調用next返回的元素的索引
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
public boolean hasNext() {
return cursor < snapshot.length;
}
......
@SuppressWarnings("unchecked")
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
}
在代碼案例中,迭代器遍歷過程時,通過hasNext()判斷集合是否遍歷完成,若還有沒遍歷的元素,就會調用 String item = (String)iterator.next()取出集合對應索引的元素。
從COWIterator類的next()方法中,可以看到,其元素是根據索引cursor從數組snapshot中取出來的。
這個snapshot就相當一個快照副本,在創建迭代器時,即new COWIterator
只需保證snapshot指向的數組地址元素不變,那麼整個迭代器讀取集合數組就不會受影響。
如何做到snapshot指向的數組地址元素不變,但是又需要同時能滿足CopyOnWriteArrayList集合的新增或者刪除操作呢?
先來看一下CopyOnWriteArrayList的 list.add("李華")操作,具體實現能夠在這塊源碼裏看到,主要以下步驟:
1、add方法用到了ReentrantLock鎖,在進行新增過程中,通過lock鎖保證線程安全。
2、Object[] elements = getArray()這裏的getArray()方法,和創建迭代器傳的參數getArray()是同一個,都是獲取到CopyOnWriteArrayList的array數組。取出array數組以及計算其長度後,創建一個比array數組長度大1的新數組,通過Arrays.copyOf(elements, len + 1)將array數組元素全部複製到新數組newElements。
3、在新數組newElements進行新增元素操作。
4、將CopyOnWriteArrayList的array數組引用指向新數組newElements,這樣array=newElements,完成新增操作。
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//獲取到CopyOnWriteArrayList的array數組
Object[] elements = getArray();
//獲取array數組長度
int len = elements.length;
//將array數組數據,全部複製到一個長度比舊數組多1的新數組裏
Object[] newElements = Arrays.copyOf(elements, len + 1);
//在新數組裏,新增一個元素
newElements[len] = e;
//將CopyOnWriteArrayList的array數組引用指向新數組newElements
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
可見,CopyOnWriteArrayList實現讀寫分離的原理,就是在COWIterator迭代器創建時,將此時的array數組指向的地址複製給snapshot,相當做了一次快照,迭代器遍歷該快照數組地址元素。
後續涉及到列表修改相關的操作,會將原始array數組全部元素複製到一個新數組上,在新數組裏面進行修改操作,這樣就不會影響到迭代器遍歷原來的數組地址裏的數據了。(這也表明,這種讀寫分離只適合讀多寫少,在寫多情況下,會出現性能問題)
新數組修改完畢後,只需將array數組引用指向新數組地址,就能完成修改操作了。
整個過程就能完成讀寫分離機制,即迭代器的Fail-Safe(安全失敗)機制。