一、單線程
1. 異常情況舉例
只要拋出出現異常,可以肯定的是代碼一定有錯誤的地方。先來看看都有哪些情況會出現ConcurrentModificationException異常,下面以ArrayList remove 操作進行舉例:
使用的數據集合:
List<string> myList = new ArrayList<string>();
myList.add( "1");
myList.add( "2");
myList.add( "3");
myList.add( "4");
myList.add( "5");
以下三種情況都會出現異常:
Iterator<string> it = myList.iterator();
while (it.hasNext()) {
String value = it.next();
if (value.equals( "3")) {
myList.remove(value); // error
}
}
for (Iterator<string> it = myList.iterator(); it.hasNext();) {
String value = it.next();
if (value.equals( "3")) {
myList.remove(value); // error
}
}
for (String value : myList) {
System. out.println( "List Value:" + value);
if (vJava ConcurrentModificationException 異常分析與解決方案ls( "3")) {
myList.remove(value); // error
}
}
異常信息如下:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.AbstractList$Itr.checkForComodification(Unknown Source)
at java.util.AbstractList$Itr.next(Unknown Source)
2. 根本原因
以上都有3種出現異常的情況有一個共同的特點,都是使用Iterator進行遍歷,且都是通過ArrayList.remove(Object) 進行刪除操作。
想要找出根本原因,直接查看ArrayList源碼看為什么出現異常:
public class ArrayList<e> extends AbstractList<e>
implements Cloneable, Serializable, RandomAccess {
@Override public boolean remove(Object object) {
Object[] a = array;
int s = size;
if (object != null) {
for (int i = 0; i < s; i++) {
if (object.equals(a[i])) {
System.arraycopy(a, i + 1, a, i, --s - i);
a[s] = null; // Prevent memory leak
size = s;
modCount++; // 只要刪除成功都是累加
return true;
}
}
} else {
for (int i = 0; i < s; i++) {
if (a[i] == null) {
System.arraycopy(a, i + 1, a, i, --s - i);
a[s] = null; // Prevent memory leak
size = s;
modCount++; // 只要刪除成功都是累加
return true;
}
}
}
return false;
}
@Override public Iterator<e> iterator() {
return new ArrayListIterator();
}
private class ArrayListIterator implements Iterator<e> {
......
// 全局修改總數保存到當前類中
/** The expected modCount value */
private int expectedModCount = modCount;
@SuppressWarnings("unchecked") public E next() {
ArrayList<e> ourList = ArrayList.this;
int rem = remaining;
// 如果創建時的值不相同,拋出異常
if (ourList.modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
if (rem == 0) {
throw new NoSuchElementException();
}
remaining = rem - 1;
return (E) ourList.array[removalIndex = ourList.size - rem];
}
......
}
}
List、Set、Map 都可以通過Iterator進行遍歷,這里僅僅是通過List舉例,在使用其他集合遍歷時進行增刪操作都需要留意是否會觸發ConcurrentModificationException異常。
3. 解決方案
上面列舉了會出現問題的幾種情況,也分析了問題出現的根本原因,現在來總結一下怎樣才是正確的,如果避免遍歷時進行增刪操作不會出現ConcurrentModificationException異常。
// 1 使用Iterator提供的remove方法,用于刪除當前元素
for (Iterator<string> it = myList.iterator(); it.hasNext();) {
String value = it.next();
if (value.equals( "3")) {
it.remove(); // ok
}
}
System. out.println( "List Value:" + myList.toString());
// 2 建一個集合,記錄需要刪除的元素,之后統一刪除
List<string> templist = new ArrayList<string>();
for (String value : myList) {
if (value.equals( "3")) {
templist.remove(value);
}
}
// 可以查看removeAll源碼,其中使用Iterator進行遍歷
myList.removeAll(templist);
System. out.println( "List Value:" + myList.toString());
// 3\. 使用線程安全CopyOnWriteArrayList進行刪除操作
List<string> myList = new CopyOnWriteArrayList<string>();
myList.add( "1");
myList.add( "2");
myList.add( "3");
myList.add( "4");
myList.add( "5");
Iterator<string> it = myList.iterator();
while (it.hasNext()) {
String value = it.next();
if (value.equals( "3")) {
myList.remove( "4");
myList.add( "6");
myList.add( "7");
}
}
System. out.println( "List Value:" + myList.toString());
// 4\. 不使用Iterator進行遍歷,需要注意的是自己保證索引正常
for ( int i = 0; i < myList.size(); i++) {
String value = myList.get(i);
System. out.println( "List Value:" + value);
if (value.equals( "3")) {
myList.remove(value); // ok
i--; // 因為位置發生改變,所以必須修改i的位置
}
}
System. out.println( "List Value:" + myList.toString());
輸出結果都是:List Value:[1, 2, 4, 5] , 不會出現異常。
以上4種解決辦法在單線程中測試完全沒有問題,但是如果在多線程中呢?
二、多線程
1. 同步異常情況舉例
上面針對ConcurrentModificationException異常在單線程情況下提出了4種解決方案,本來是可以很哈皮的洗洗睡了,但是如果涉及到多線程環境可能就不那么樂觀了。
下面的例子中開啟兩個子線程,一個進行遍歷,另外一個有條件刪除元素:
final List<string> myList = createTestData();
new Thread(new Runnable() {
@Override
public void run() {
for (String string : myList) {
System.out.println("遍歷集合 value = " + string);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (Iterator<string> it = myList.iterator(); it.hasNext();) {
String value = it.next();
System.out.println("刪除元素 value = " + value);
if (value.equals( "3")) {
it.remove();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
輸出結果:
遍歷集合 value = 1
刪除元素 value = 1
遍歷集合 value = 2
刪除元素 value = 2
遍歷集合 value = 3
刪除元素 value = 3
Exception in thread "Thread-0" 刪除元素 value = 4
java.util.ConcurrentModificationException
at java.util.AbstractListItr.next(Unknown Source)
at list.ConcurrentModificationExceptionStudy$1.run(ConcurrentModificationExceptionStudy.java:42)
at java.lang.Thread.run(Unknown Source)
刪除元素 value = 5
結論:
上面的例子在多線程情況下,僅使用單線程遍歷中進行刪除的第1種解決方案使用it.remove(),但是測試得知4種的解決辦法中的1、2、3依然會出現問題。
接著來再看一下JavaDoc對java.util.ConcurrentModificationException異常的描述:
當方法檢測到對象的并發修改,但不允許這種修改時,拋出此異常。
說明以上辦法在同一個線程執行的時候是沒問題的,但是在異步情況下依然可能出現異常。
2. 嘗試方案
(1) 在所有遍歷增刪地方都加上synchronized或者使用Collections.synchronizedList,雖然能解決問題但是并不推薦,因為增刪造成的同步鎖可能會阻塞遍歷操作。
(2) 推薦使用ConcurrentHashMap或者CopyOnWriteArrayList。
3. CopyOnWriteArrayList注意事項
(1) CopyOnWriteArrayList不能使用Iterator.remove()進行刪除。
(2) CopyOnWriteArrayList使用Iterator且使用List.remove(Object);會出現如下異常:
java.lang.UnsupportedOperationException: Unsupported operation remove
at java.util.concurrent.CopyOnWriteArrayList$ListIteratorImpl.remove(CopyOnWriteArrayList.java:804)
4. 解決方案
單線程情況下列出4種解決方案,但是發現在多線程情況下僅有第4種方案才能在多線程情況下不出現問題。
List<string> myList = new CopyOnWriteArrayList<string>();
myList.add( "1");
myList.add( "2");
myList.add( "3");
myList.add( "4");
myList.add( "5");
new Thread(new Runnable() {
@Override
public void run() {
for (String string : myList) {
System.out.println("遍歷集合 value = " + string);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < myList.size(); i++) {
String value = myList.get(i);
System.out.println("刪除元素 value = " + value);
if (value.equals( "3")) {
myList.remove(value);
i--; // 注意
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
輸出結果:
刪除元素 value = 1
遍歷集合 value = 1
刪除元素 value = 2
遍歷集合 value = 2
刪除元素 value = 3
遍歷集合 value = 3
刪除元素 value = 4
遍歷集合 value = 4
刪除元素 value = 5
遍歷集合 value = 5
OK,搞定