tips:集合之併發修改異常(ConcurrentModificationException)
1-背景
- 在用迭代器遍歷一個集合對象時,如果遍歷過程中對集合對象的內容進行了修改(增加、刪除、修改),則會拋出ConcurrentModificationException。
2-原理
- 迭代器在遍歷時會直接訪問集合中的內容,並且在遍歷過程中使用一個 modCount 變量。集合在被遍歷期間如果內容發生變化,就會改變modCount的值。每當迭代器使用hashNext()/next()遍歷下一個元素之前,都會檢測modCount變量是否爲expectedmodCount值,是的話就返回遍歷;否則拋出異常,終止遍歷。
代碼: 增強foreach遍歷集合
List<Integer> list = new ArrayList<>();
list.add(12);
list.add(13);
list.add(14);
for(Integer value:list){
if(value.intValue()==13){
list.remove(value);
}
}
上述代碼: java for增強遍歷集合 會重寫方法等同於iterator遍歷集合
List<Integer> list = new ArrayList<>();
list.add(12);
list.add(13);
list.add(14);
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
// 拋ConcurrentModificationException異常的點: it.next()方法 分析源碼
Integer value = it.next();
if(value.intValue()==12){
list.remove(value);
}
}
分析it.next()此處實現源碼 【代碼位於ArrayList源碼的內部類裏面】
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();
}
}
// 異常拋出最終位置 【條件: modCount != expectedModCount】
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
追溯變量modCount 與 expectedModCount
- 查看list方法的源碼,發現集合list每次操作add,remove,clear都會增加modCount值
public E remove(int index) { ... modCount++; ... }
- 而expectedModCount值在ArrayList的內部類Itr中,在聲明初始化時候會將modCount賦值給它
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;
- 所以在上述foreach或者iterator遍歷集合時,針對集合list.remove(12)操作後,此時modCount++,expectedModCount值保持不變,所以iterator.next()指向下一個元素時候檢查並拋出該異常。
- 核心代碼:
Iterator<Integer> it = list.iterator(); while(it.hasNext()){ // 拋ConcurrentModificationException異常的點: it.next()方法 分析源碼 Integer value = it.next(); if(value.intValue()==12){ list.remove(value); } }
ArrayList內部類的remove()方法
- 解決方法
- 單線程scenes
Iterator<Integer> iterator2 = list.iterator(); while(iterator2.hasNext()){ Integer value = iterator2.next(); if(value.intValue()==13){ iterator2.remove(); } }
- 解釋:上面看到ArrayList的內部類有remove()方法,首先調用ArrayList.remove(index)方法,接着進行賦值操作expectedModCount = modCount 即可保證兩者相等
- 核心代碼
... ArrayList.this.remove(lastRet); ... expectedModCount = modCount;
- 多線程scenes
- 如果兩個線程沒有同步鎖或者其他措施,也容易產生上述異常,因爲一個線程操作時候更改了modCount值,而另外一個迭代時該迭代器的expectedModCount與modCOunt值不一致。解決方法如下:
- 01 迭代前加鎖,解決了多線程問題,但還是不能進行迭代add、clear等操作
new Thread( ()->{ synchronized (list){ Iterator<Integer> it01 = list.iterator(); while(it01.hasNext()){ System.out.println(Thread.currentThread().getName()+" "+it01.next()); try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } } ).start(); new Thread(()->{ synchronized (list){ Iterator<Integer> it02 = list.iterator(); while(it02.hasNext()){ System.out.println(Thread.currentThread().getName()+" "+it02.next()); if(it02.next().intValue()==14){ it02.remove(); } } } } ).start();
- 02 採用CopyOnWriteArrayList,解決了多線程問題,同時可以add、clear等操作;原理:CopyOnWriteArrayList也是一個線程安全的ArrayList,其實現在於每次add,remove等所有的操作都是重新創建一個新的數組,再把引用指向新的數組。
// 核心代碼 static List<String> list = new CopyOnWriteArrayList<String>(); public static void main(String[] args) { list.add("a"); list.add("b"); list.add("c"); list.add("d"); new Thread() { public void run() { Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { System.out.println(Thread.currentThread().getName() + ":" + iterator.next()); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }; }.start(); new Thread() { public synchronized void run() { Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String element = iterator.next(); System.out.println(Thread.currentThread().getName() + ":" + element); if (element.equals("c")) { list.remove(element); } } }; }.start(); }
3-疑惑
- 1.既然modCount與expectedModCount不同會產生異常,那爲什麼還設置這個變量
- 解釋:
- fast-fail與fail-safe
- 參考2
- 源碼內部有錯誤檢測機制:核心fail-fast即用來檢查多線程對線程進行操作造成併發問題。
- ConcurrentModificationException併發修改異常。
- 解釋: