foreach中不允許對元素進行add和remove底層原理
🍅 Java學習路線:搬磚工的Java學習路線
🍅 作者微信公眾號:程式員小王
🍅 程式員小王的博客:https://www.wolai.com/wnaghengjie/ahNwvAUPG2Hb1Sy7Z8waaF
🍅 掃描主頁左側二維碼,加我微信 一起學習、一起進步
🍅 歡迎點贊 👍 收藏 ?留言 📝
一、foreach遍歷ArrayList程序中使用 add 和 remove
foreach遍歷ArrayList代碼:
@Test
public void ArrayList(){
ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i <10; i++) {
list.add(i);
}
for (Integer integer : list) {
if(integer==3){
list.remove(integer);
}
System.out.println(integer);
}
}
結果:

結果是出現了ConcurrentModificationException 例外,追蹤下拋出例外的位置checkForComodification(ArrayList.java:901)

這個地方告訴我們如果 modCount 不等于 expectedModCount 的時候,就會拋出這個例外資訊,那么這兩個引數都代表了什么東西呢?為什么不相等的時候,就會出現例外呢?
二、追根溯源
1、modCount是什么?
-
單詞翻譯:modCount 修改計數
-
底層原理:這時候就要讓我們去看原始碼了,在我們點到這個變數的時候,就會有注釋告訴我們了 modCount 是 AbstractList 類中的一個成員變數 ,該值表示對List的修改次數,

2、expectedModCount 是什么?
-
單詞翻譯:expectedModCount 期望的最大計數
-
底層原理:expectedModCount 是 ArrayList 中的一個內部類——Itr中的成員變數,我們來看下怎么又扯出個內部類Itr,通過反編譯可以發現foreach編譯后內部是使用迭代器實作 的,

從原始碼中可以看到Itr實作了Iterator介面,同時宣告了expectedModCount這個成員變數, expectedModCount 表示對ArrayList修改次數的期望值,它的初始值為 modCount,
3、熟悉的checkForComodification方法
-
checkForComodification翻譯:所有方法中都進行了并發 修改的檢測
-
從原始碼可以看到這個類的next和remove方法里面都呼叫了一個checkForComodification方法,看到checkForComodification是不是很熟悉,這不就是例外的拋出位置嗎,

checkForComodification方法是通過判斷modCount和expectedModCount 是否相等來決定是否拋出并發修改例外,

4、流程回顧
大致流程如下:當integer為3時,呼叫了remove方法,remove方法中修改了modCount值,然后再輸出integer值,再進入下一次回圈,此時hasNext為true,進入回圈體第一行代碼,呼叫next方法,next方法再呼叫checkForComodification方法,然后發現expectedModCount和modCount不一致,最終拋出ConcurrentModificationException 例外,

也就是說,expectedModCount 初始化為 modCount 了,但是后面expectedModCount 沒有修改,而在 remove 和 add 的程序中修改了modCount ,這就導致了執行的時候,通過 checkForComodification 方法來判斷兩個值是否相等,如果相等了,那么沒問題,如果不相等,那就給你拋出一個例外來,
而這也就是我們通俗說起來的 fail-fast 機制,也就是快速檢測失敗機制,
(1)什么是fail-fast機制?
fail-fast 機制,即快速失敗機制,是java集合(Collection)中的一種錯誤檢測機制,當在迭代集合的程序中該集合在結構上發生改變的時候,就有可能會發生fail-fast,即拋出 ConcurrentModificationException 例外,fail-fast機制并不保證在不同步的修改下一定會拋出例外,它只是盡最大努力去拋出,所以這種機制一般僅用于檢測bug,
2、fail-fast的出現場景
在我們常見的java集合中就可能出現fail-fast機制,比如ArrayList,HashMap,在多執行緒和單執行緒環境下都有可能出現快速失敗,
三、避免fail-fast 機制
將上面的代碼進行修改:
(1)迭代器實作remove/add
@Test
public void ArrayList(){
ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i <10; i++) {
list.add(i);
}
System.out.println("沒有洗掉元素前"+list.toString());
//使用迭代器的方向進行remove、add
ListIterator<Integer> listIterator = list.listIterator();
while(listIterator.hasNext()){
Integer integer = listIterator.next();
if(integer==3){
listIterator.remove();
listIterator.add(11);
}
}
System.out.println("洗掉元素后"+list.toString());
}
- 運行結果:

結果也是顯而易見的,我們實作了在 foreach 中進行 add 和 remove 的操作.
這里有個注意點,迭代器使用listIterator和iterator均可,看原始碼可以知道 listIterator其實使用的ListItr內部類,ListItr是繼承了Itr類的,同時自己封了一些方法,例如add,hasPrevious,previous等等,所以代碼中的remove方法是Itr類的,add方法是ListItr類的
(2)listIterator和iterator區別:
-
使用范圍不同,Iterator可以應用于所有的集合,Set、List和Map和這些集合的子型別,而ListIterator只能用于List及其子型別,
-
ListIterator有add方法,可以向List中添加物件,而Iterator不能,
-
ListIterator和Iterator都有hasNext()和next()方法,可以實作順序向后遍歷,但是ListIterator有hasPrevious()和previous()方法,可以實作逆向(順序向前)遍歷,Iterator不可以,
-
ListIterator可以定位當前索引的位置,nextIndex()和previousIndex()可以實作,Iterator沒有此功能,
-
都可實作洗掉操作,但是ListIterator可以實作物件的修改,set()方法可以實作,Iterator僅能遍歷,不能修改,
四、使用CopyOnWriteArrayList
1、CopyOnWriteArrayList介紹
在很多應用場景中,讀操作可能會遠遠大于寫操作,由于讀操作根本不會修改原有的資料,
因此如果每次讀取都進行加鎖操作,其實是一種資源浪費,
我們應該允許多個執行緒同時訪問 List 的內部資料,畢竟讀操作是執行緒安全的,
這和 ReentrantReadWriteLock 讀寫鎖的思想非常類似,
也就是 讀讀共享、寫寫互斥、讀寫互斥、寫讀互斥,
JDK中提供了 CopyOnWriteArrayList 類,
相比于在讀寫鎖的思想又更進一步,為了將讀取的性能發揮到極致,
CopyOnWriteArrayList 讀取是完全不用加鎖的,并且更厲害的是:
寫入也不會阻塞讀取操作,只有寫入和寫入之間需要進行同步等待,讀操作的性能得到大幅度提升,
CopyOnWriteArrayList 這個類也是能解決 fail-fast 的問題的,我們來試一下:
@Test
public void copyOnWriteArrayList(){
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
for (int i = 0; i <5; i++) {
list.add(i);
}
System.out.println("沒有洗掉元素前"+list);
for (Integer integer : list) {
if(integer==3){
list.remove(integer);
list.add(10);
}
}
System.out.println("洗掉元素后"+list);
}

2、CopyOnWriteArrayList實作add/remove原理:
CopyOnWriteArrayList實作了對這個元素中間進行移除添加 的操作,那么他的內部原始碼是怎么實作的,實際上很簡單,復制
也就是他創建一個新的陣列,再將舊的陣列復制到新的陣列上 ,但是為什么很少有人推薦這種做法,根本原因還是 復制
因為你使用了復制,那么就一定會出現有兩個存盤相同內容的空間,這樣消耗了空間,最后進行 GC 的時候,那是不是也需要一些時間去清理他,所以個人不是很推薦,但是寫出來的必要還是有的,
(1)CopyOnWriteArrayList實作add底層原理:
public boolean add(E e) {
// 可重入鎖
final ReentrantLock lock = this.lock;
// 獲取鎖
lock.lock();
try {
// 元素陣列
Object[] elements = getArray();
// 陣列長度
int len = elements.length;
// 復制陣列
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 存放元素e
newElements[len] = e;
// 設定陣列
setArray(newElements);
return true;
} finally {
// 釋放鎖
lock.unlock();
}
}
處理流程如下:
1.獲取鎖(保證多執行緒的安全訪問),獲取當前的Object陣列,獲取Object陣列的長度為length,進入步驟②,
2.根據Object陣列復制一個長度為length+1的Object陣列為newElements
(此時,newElements[length]為null),進入下一步驟,
3.將下標為length的陣列元素newElements[length]設定為元素e,
再設定當前Object[]為newElements,釋放鎖,回傳,這樣就完成了元素的添加,
(2)CopyOnWriteArrayList實作remove原理:
public E remove(int index) {
// 可重入鎖
final ReentrantLock lock = this.lock;
// 獲取鎖
lock.lock();
try {
// 獲取陣列
Object[] elements = getArray();
// 陣列長度
int len = elements.length;
// 獲取舊值
E oldValue = get(elements, index);
// 需要移動的元素個數
int numMoved = len - index - 1;
if (numMoved == 0) // 移動個數為0
// 復制后設定陣列
setArray(Arrays.copyOf(elements, len - 1));
else { // 移動個數不為0
// 新生陣列
Object[] newElements = new Object[len - 1];
// 復制index索引之前的元素
System.arraycopy(elements, 0, newElements, 0, index);
// 復制index索引之后的元素
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
// 設定索引
setArray(newElements);
}
// 回傳舊值
return oldValue;
} finally {
// 釋放鎖
lock.unlock();
}
}
處理流程如下:
1.獲取鎖,獲取陣列elements,陣列長度為length,獲取索引的值elements[index],
計算需要移動的元素個數(length - index - 1),若個數為0,則表示移除的是陣列的最后一個元素,
復制elements陣列,復制長度為length-1,然后設定陣列,進入步驟③;否則,進入步驟②
2.先復制index索引前的元素,再復制index索引后的元素,然后設定陣列,
3.釋放鎖,回傳舊值,
3、注意
CopyOnWriteArrayList解決 fail-fast 的問題不是通過迭代器來remove或add元素的,而是通過list本身的remove和add方法,所以add的元素位置也不一樣,迭代器是當前位置后面一個,CopyOnWriteArrayList是直接放到最后 ,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/340729.html
標籤:java
