淺析Java對集合進行操作時報java.util.ConcurrentModificationException併發修改異常問題:產生原因、單執行緒/多執行緒環境解決、CopyOnWriteArrayList執行緒安全的ArrayList、fail-fast快速失敗機制防止多執行緒修改集合造成併發問題
一、異常產生
當我們迭代一個ArrayList或者HashMap時,如果嘗試對集合做一些修改操作(例如刪除元素),可能會丟擲java.util.ConcurrentModificationException
的異常。
import java.util.Iterator;
import java.util.List;
public class AddRemoveListElement {
public static void main(String args[]) {
List<String> list = new ArrayList<String>();
list.add( "A");
list.add("B");
for (String s : list) {
if (s.equals("B")) {
list.remove(s);
}
}
//foreach迴圈等效於迭代器
/*Iterator<String> iterator=list.iterator();
while(iterator.hasNext()){
String s=iterator.next();
if (s.equals("B")) {
list.remove(s);
}
} */
}
}
二、異常原因
ArrayList的父類AbstarctList中有一個域 modCount
,每次對集合進行修改(增添元素,刪除元素……)時都會 modCount++
,而 foreach 的背後實現原理其實就是Iterator(關於Iterator可以看Java Design Pattern: Iterator),等同於註釋部分程式碼。在這裡,迭代ArrayList的Iterator中有一個變數 expectedModCount
,該變數會初始化和 modCount
相等,但如果接下來如果集合進行修改 modCount
改變,就會造成 expectedModCount!=modCount
我們再來根據原始碼詳細的走一遍這個過程
/*
*AbstarctList的內部類,用於迭代
*/
private class Itr implements Iterator<E> {
int cursor = 0; //將要訪問的元素的索引
int lastRet = -1; //上一個訪問元素的索引
int expectedModCount = modCount;//expectedModCount為預期修改值,初始化等於modCount(AbstractList類中的一個成員變數)
//判斷是否還有下一個元素
public boolean hasNext() {
return cursor != size();
}
//取出下一個元素
public E next() {
checkForComodification(); //關鍵的一行程式碼,判斷expectedModCount和modCount是否相等
try {
E next = get(cursor);
lastRet = cursor++;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
public void remove() {
if (lastRet == -1)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
根據程式碼可知,每次迭代list時,會初始化Itr的三個成員變數:cursor
、lastRe、
expectedModCount = modCount;
接著呼叫hasNext()
迴圈判斷訪問元素的下標是否到達末尾。如果沒有,呼叫next()
方法,取出元素。
而最上面測試程式碼出現異常的原因在於,next()
方法呼叫checkForComodification()
時,發現了modCount != expectedModCount
public boolean add(E paramE) {
ensureCapacityInternal(this.size + 1);
/** 省略此處程式碼 */
}
private void ensureCapacityInternal(int paramInt) {
if (this.elementData == EMPTY_ELEMENTDATA)
paramInt = Math.max(10, paramInt);
ensureExplicitCapacity(paramInt);
}
private void ensureExplicitCapacity(int paramInt) {
this.modCount += 1; //修改modCount
/** 省略此處程式碼 */
}
public boolean remove(Object paramObject) {
int i;
if (paramObject == null)
for (i = 0; i < this.size; ++i) {
if (this.elementData[i] != null)
continue;
fastRemove(i);
return true;
}
else
for (i = 0; i < this.size; ++i) {
if (!(paramObject.equals(this.elementData[i])))
continue;
fastRemove(i);
return true;
}
return false;
}
private void fastRemove(int paramInt) {
this.modCount += 1; //修改modCount
/** 省略此處程式碼 */
}
public void clear() {
this.modCount += 1; //修改modCount
/** 省略此處程式碼 */
}
從上面的程式碼可以看出,ArrayList的add、remove、clear方法都會造成modCount的改變。迭代過程中如果呼叫這些方法就會造成modCount的增加,使迭代類中expectedModCount和modCount不相等。
三、異常的解決
1、單執行緒環境 —— 單執行緒可以使用 迭代器的 remove 方法現在我們已經基本瞭解了異常的發生原因了。接下來我們來解決它。我很任性,我就是想在迭代集合時刪除集合的元素,怎麼辦?
Iterator<String> iter = list.iterator();
while(iter.hasNext()){
String str = iter.next();
if( str.equals("B") ){
iter.remove();
}
}
細心的朋友會發現 Itr 中的也有一個remove方法,實質也是呼叫了ArrayList中的remove,但增加了expectedModCount = modCount;
保證了不會丟擲java.util.ConcurrentModificationException異常。
但是,這個辦法的有兩個弊端
(1)只能進行remove操作,add、clear等Itr中沒有。
(2)而且只適用單執行緒環境。
2、多執行緒環境 —— 需要使用 CopyOnWriteArrayList
在多執行緒環境下,我們再次試驗下上面的程式碼
public class Test2 {
static List<String> list = new ArrayList<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) {
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")) {
iterator.remove();
}
}
};
}.start();
}
}
異常的原因很簡單,一個執行緒修改了list的modCount導致另外一個執行緒迭代時modCount與該迭代器的expectedModCount不相等。
此時有兩個辦法:
(1)迭代前加鎖,解決了多執行緒問題,但還是不能進行迭代add、clear等操作
(2)採用 CopyOnWriteArrayList,解決了多執行緒問題,同時可以add、clear等操作
static List<String> list = new CopyOnWriteArrayList<String>();
public class Test2 {
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();
}
}
CopyOnWriteArrayList也是一個執行緒安全的ArrayList,其實現原理在於,每次add,remove等所有的操作都是重新建立一個新的陣列,再把引用指向新的陣列。由於我用CopyOnWriteArrayList少,這裡就不多討論了,想了解可以看:Java併發程式設計:併發容器之CopyOnWriteArrayList
四、深入理解異常—fail-fast機制
到這裡,我們似乎已經理解完這個異常的產生緣由了。但是,仔細思考,還是會有幾點疑惑:
1、既然modCount與expectedModCount不同會產生異常,那為什麼還設定這個變數
2、ConcurrentModificationException可以翻譯成“併發修改異常”,那這個異常是否與多執行緒有關呢?
我們來看看原始碼中 modCount 的註解。
/**
* The number of times this list has been <i>structurally modified</i>.
* Structural modifications are those that change the size of the
* list, or otherwise perturb it in such a fashion that iterations in
* progress may yield incorrect results.
*
* <p>This field is used by the iterator and list iterator implementation
* returned by the {@code iterator} and {@code listIterator} methods.
* If the value of this field changes unexpectedly, the iterator (or list
* iterator) will throw a {@code ConcurrentModificationException} in
* response to the {@code next}, {@code remove}, {@code previous},
* {@code set} or {@code add} operations. This provides
* <i>fail-fast</i> behavior, rather than non-deterministic behavior in
* the face of concurrent modification during iteration.
*
* <p><b>Use of this field by subclasses is optional.</b> If a subclass
* wishes to provide fail-fast iterators (and list iterators), then it
* merely has to increment this field in its {@code add(int, E)} and
* {@code remove(int)} methods (and any other methods that it overrides
* that result in structural modifications to the list). A single call to
* {@code add(int, E)} or {@code remove(int)} must add no more than
* one to this field, or the iterators (and list iterators) will throw
* bogus {@code ConcurrentModificationExceptions}. If an implementation
* does not wish to provide fail-fast iterators, this field may be
* ignored.
*/
protected transient int modCount = 0;
我們注意到,註解中頻繁的出現了fail-fast,
那麼fail-fast
(快速失敗)機制是什麼呢?
“快速失敗”也就是fail-fast,它是Java集合的一種錯誤檢測機制。
當多個執行緒對集合進行結構上的改變的操作時,有可能會產生fail-fast機制。
記住是有可能,而不是一定。
例如:假設存在兩個執行緒(執行緒1、執行緒2),執行緒1通過Iterator在遍歷集合A中的元素,在某個時候執行緒2修改了集合A的結構(是結構上面的修改,而不是簡單的修改集合元素的內容),那麼這個時候程式就會丟擲 ConcurrentModificationException 異常,從而產生fail-fast機制。
看到這裡,我們明白了,fail-fast機制就是為了防止多執行緒修改集合造成併發問題的機制嘛。
之所以有modCount這個成員變數,就是為了辨別多執行緒修改集合時出現的錯誤。而java.util.ConcurrentModificationException就是併發異常。但是單執行緒使用不當時也可能丟擲這個異常。
原文連結:https://www.jianshu.com/p/c5b52927a61a