其實我們日常學習的知識點和問題都是一環扣一環的,很多問題都是相關聯的,本次主要圍繞着以下問題展開 :
- ArrayList的
remove(Object o)
方法 與Iterator 的remove()
方法有什麼區別?(會引出Fail-Fast機制)
- 什麼是Fail-Fast機制與Fail-Safe機制機制?
(會引出CopyOnWrite思想)
- 什麼是CopyOnWrite思想?
- CopyOnWrite的使用場景與優缺點?
其實我們平時也會遇到這樣的需求:將列表中滿足某種條件的數據刪除;稍後讓我們藉助下面這個邏輯來簡單描述下,就是將列表中將等於"1002"的元素刪除,此處就拿遍歷ArrayList集合當作事例,代碼如下:
private static void testUnsafeArrayList() {
List<String> arr = new ArrayList<>();
for (int i = 0; i < 10; i++) {
arr.add("100"+i);
}
Iterator it = arr.iterator();
while (it.hasNext()) {
String value = String.valueOf(it.next());
if ("1002".equals(value)){
arr.remove(value); //使用這個會拋異常ConcurrentModificationException
// it.remove(); //arr.remove(value)替換成 it.remove()沒異常
}
}
System.out.println("刪除後的數據:");
Iterator tmp = arr.iterator();
while (tmp.hasNext()) {
String value = String.valueOf(tmp.next());
System.out.println(value);
}
}
運行結果:
執行代碼使用 arr.remove(value)
你會發現報錯如下:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.liujc.demo.TestUtil.testUnsafeArrayList(TestUtil.java:69)
at com.liujc.demo.TestUtil.main(TestUtil.java:38)
Process finished with exit code 1
替換方案:
然後使用it.remove()
替換arr.remove(value)
即可正常運行:
這是什麼原因呢?
原因是迭代器在遍歷時直接訪問集合中的內容,並且在遍歷過程中使用一個 modCount 變量。集合在被遍歷期間如果內容發生變化,就會改變 modCount 的值。
每當迭代器使用 hashNext()/next() 遍歷下一個元素之前,都會檢測 modCount 變量是否爲 expectedModCount 值,相等的話就返回遍歷;否則拋出異常,終止遍歷。
這個理由說的似乎有點道理,我要眼見爲實。
接下來讓我們根據源碼來分析下:
首先我們上面代碼中利用了ArrayList的remove方法會拋出異常,其實在remove
方法中調用了fastRemove
, 在fastRemove
中執行modCount++;
更新了更改次數,查看源碼:
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
//此處記修改次數
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
在依次遍歷迭代器Iterator 時調用其next()
方法,通過下面next()
源碼中可以看到首先調用checkForComodification()
方法,該方法主要判斷 expectedModCount
是否與 modCount
相等,不相等拋出異常:
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() {
//此處的modCount已經在remove操作中變更
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
替換方案中使用迭代器的remove方法爲什麼沒有問題?
查看ArrayList的源碼中ListItr
繼承了Itr
,對應的remove()
中執行expectedModCount = modCount
對expectedModCount
重新賦值繞過後續檢查,源碼如下:
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
//此處重新賦值,會繞過後續next()方法中的checkForComodification檢查條件
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
其實我們在遇到問題時儘量做到不但要知其然還要知其所以然,要有這種探索精神,但是看源碼有時候也要點到爲止
。好了,迴歸正題,上面這個案例分析其實已經描述了Fail-Fast 機制的實現原理,那麼現在對Fail-Fast 機制是否瞭解了呢?
什麼是快速失敗機制(Fail-Fast)和 安全失敗機制(Fail-Safe)?
也許日常沒太注意過這兩種機制,但是我們肯定都碰到過,其實在我們平時接觸的HashMap、ArrayList 這些集合類,這些在 java.util 包的集合類就都是快速失敗的(Fail-Fast);而 java.util.concurrent 包下的類都是安全失敗(Fail-Safe),比如:CopyOnWriteArrayList等。
Fail-Fast機制
-
什麼是Fail-Fast機制?
Fail-fast 機制是 Java 集合中的一種錯誤機制。
在使用迭代器對集合對象進行遍歷的時候,如果 A 線程正在對集合進行遍歷,此時 B 線程對集合進行修改(增加、刪除),或者 A 線程在遍歷過程中對集合進行修改,都會導致 A 線程拋出ConcurrentModificationException
異常。 -
Fail-fast 機制實現原理
對於實現原理其實根據上面的案例分析更清楚些,這裏簡單描述下:
迭代器在遍歷時直接訪問集合中的內容,並且在遍歷過程中使用一個modCount
變量。集合在被遍歷期間如果內容發生變化,就會改變modCount
的值。
每當迭代器使用 hashNext()/next() 遍歷下一個元素之前,都會檢測modCount
變量是否與expectedModCount
值相等,相等的話就返回遍歷;否則拋出異常,終止遍歷。其實在構造迭代器Iterator
時,通過expectedModCount
記錄了集合當前(開始遍歷元素之前)的修改次數: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; //首次初始化迭代器時兩者賦值相等,兩者不等時說明遍歷過程集合數據被修改 Itr() {} 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(); } }
Fail-Safe機制
-
什麼是Fail-Safe機制?
採用安全失敗機制的集合容器,在遍歷時不是直接在集合內容上訪問的,而是先複製原有集合內容,在拷貝的集合上進行遍歷。
由於迭代時是對原集合的拷貝進行遍歷,所以在遍歷過程中對原集合所作的修改並不能被迭代器檢測到,故不會拋ConcurrentModificationException
異常。 -
Fail-Safe機制原理:
在原集合的 copy 上遍歷,也就是運用CopyOnWrite思想。 -
缺點:
- 創建原集合的 copy 需要額外的空間和時間上的開銷;
- 可能存在髒讀的數據,不能保證遍歷的是最新的內容。
CopyOnWrite思想(寫入時複製)
-
CopyOnWrite思想概念
是一種計算機程序設計領域的優化策略。其核心思想是,如果有多個調用者同時請求相同資源(如內存或磁盤上的數據存儲),他們會共同獲取相同的指針指向相同的資源,直到某個調用者試圖修改資源的內容時,系統纔會真正複製一份專用副本給該調用者,而其他調用者所見到的最初的資源仍然保持不變。這過程對其他的調用者都是透明無感知的。此作法主要的優點是如果調用者沒有修改該資源,就不會有副本被創建,因此多個調用者只是讀取操作時可以共享同一份資源。 -
CopyOnWrite容器
CopyOnWrite容器即寫時複製的容器。通俗的理解是當我們往一個容器添加元素的時候,不直接往當前容器添加,而是先將當前容器進行Copy,複製出一個新的容器,然後新的容器裏添加元素,添加完元素之後,再將原容器的引用指向新的容器。這樣做的好處是我們可以對CopyOnWrite容器進行併發的讀,而不需要加鎖,因爲當前容器不會添加任何元素。所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器。 -
CopyOnWriteArrayList分析
-
概念:
併發版ArrayList,底層結構也是數組,和ArrayList不同之處在於:當新增和刪除元素時會創建一個新的數組,在新的數組中增加或者排除指定對象,最後用新增數組替換原來的數組。 -
適用場景:
由於讀操作不加鎖,寫(增、刪、改)操作加鎖,因此適用於讀多寫少的場景。 -
侷限:
由於讀的時候不會加鎖(讀的效率高,就和普通ArrayList一樣),讀取的當前副本,因此可能讀取到髒數據。
-
概念:
CopyOnWriteArrayList的整個add操作都是在鎖的保護下進行的。
這樣做是爲了避免在多線程併發add的時候,複製出多個副本出來,可能會出現髒讀的情況,導致最終的數組數據不是我們期望的。
public boolean add(E e) {
//1、先加鎖
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//2、拷貝數組
Object[] newElements = Arrays.copyOf(elements, len + 1);
//3、將元素加入到新數組中
newElements[len] = e;
//4、將array引用指向到新數組
setArray(newElements);
return true;
} finally {
//5、解鎖
lock.unlock();
}
}
線程併發的寫,則通過鎖來控制,如果有線程併發的讀,則分以下幾種情況:
1、如果寫操作未完成,那麼直接讀取原數組的數據;
2、如果寫操作完成,但是引用還未指向新數組,那麼也是讀取原數組數據;
3、如果寫操作完成,並且引用已經指向了新的數組,那麼直接從新數組中讀取數據。