異常產生
當我們迭代一個ArrayList或者HashMap時,如果嘗試對集合做一些修改操作(例如刪除元素),可能會丟擲java.util.ConcurrentModificationException
的異常。
package reyo.sdk.utils.test.list2;
import java.util.ArrayList;
import java.util.List;
public class AddRemoveList {
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
,此時就會丟擲java.util.ConcurrentModificationException異常
過程如下圖:
我們再來根據原始碼詳細的走一遍這個過程
/*
*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();
}
}
作者:MrDTree
連結:http://www.jianshu.com/p/c5b52927a61a
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。
根據程式碼可知,每次迭代list時,會初始化Itr的三個成員變數
int cursor = 0; //將要訪問的元素的索引
int lastRet = -1; //上一個訪問元素的索引
int expectedModCount = modCount; //預期修改值,初始化等於modCount(AbstractList類中的一個成員變數)
接著呼叫hasNext()
迴圈判斷訪問元素的下標是否到達末尾。如果沒有,呼叫next()
方法,取出元素。
而最上面測試程式碼出現異常的原因在於,next()
方法呼叫checkForComodification()
時,發現了modCount != expectedModCount
接下來我們看下ArrayList的原始碼,瞭解下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. 單執行緒環境
好,現在我們已經基本瞭解了異常的傳送原因了。接下來我們來解決它。
我很任性,我就是想在迭代集合時刪除集合的元素,怎麼辦?
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. 多執行緒環境
在多執行緒環境下,我們再次試驗下上面的程式碼
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等操作
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();
synchronized (list) {
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();
synchronized (list) {
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(Thread.currentThread().getName()
+ ":" + element);
if (element.equals("c")) {
iterator.remove();
}
}
}
};
}.start();
}
}
2,採用CopyOnWriteArrayList,解決了多執行緒問題,同時可以add、clear等操作
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等所有的操作都是重新建立一個新的陣列,再把引用指向新的陣列。
深入理解異常—fail-fast機制
到這裡,我們似乎已經理解完這個異常的產生緣由了。
但是,仔細思考,還是會有幾點疑惑:
- 既然modCount與expectedModCount不同會產生異常,那為什麼還設定這個變數
- 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機制。
之所以有modCount這個成員變數,就是為了辨別多執行緒修改集合時出現的錯誤。而java.util.ConcurrentModificationException就是併發異常。
但是單執行緒使用不單時也可能丟擲這個異常。