java.util.ConcurrentModificationException

java.util.ConcurrentModificationException 是在使用 Java 集合類別時非常常見的例外情況。Java 集合類別是 fail-fast 的,這意味著如果在使用迭代器遍歷集合的某個線程正在進行時,集合發生了改變,則 iterator.next() 會拋出 ConcurrentModificationException。在多線程和單線程的 Java 程式環境中都可能發生並發修改異常。

java.util.ConcurrentModificationException

讓我們通過一個例子來看看並發修改異常的情況。

package com.journaldev.ConcurrentModificationException;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public class ConcurrentModificationExceptionExample {

	public static void main(String args[]) {
		List myList = new ArrayList();

		myList.add("1");
		myList.add("2");
		myList.add("3");
		myList.add("4");
		myList.add("5");

		Iterator it = myList.iterator();
		while (it.hasNext()) {
			String value = it.next();
			System.out.println("List Value:" + value);
			if (value.equals("3"))
				myList.remove(value);
		}

		Map myMap = new HashMap();
		myMap.put("1", "1");
		myMap.put("2", "2");
		myMap.put("3", "3");

		Iterator it1 = myMap.keySet().iterator();
		while (it1.hasNext()) {
			String key = it1.next();
			System.out.println("Map Value:" + myMap.get(key));
			if (key.equals("2")) {
				myMap.put("1", "4");
				// myMap.put("4", "4");
			}
		}

	}
}

上面的程式在執行時會拋出 java.util.ConcurrentModificationException,如下面的控制台日誌所示。

List Value:1
List Value:2
List Value:3
Exception in thread "main" java.util.ConcurrentModificationException
	at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:937)
	at java.base/java.util.ArrayList$Itr.next(ArrayList.java:891)
	at com.journaldev.ConcurrentModificationException.ConcurrentModificationExceptionExample.main(ConcurrentModificationExceptionExample.java:22)

從輸出的堆疊跟蹤可以清楚地看出,在調用迭代器的 next() 函數時會拋出並行修改異常。如果你想知道迭代器是如何檢查修改的,它的實現存在於 AbstractList 類中,其中定義了一個 int 變量 modCount。modCount 提供了列表大小已更改的次數。modCount 的值在每次 next() 調用中被用來檢查函數 checkForComodification() 中的任何修改。現在,註釋掉列表部分,然後再次運行程序。你會發現現在不再拋出 ConcurrentModificationException。輸出:

Map Value:3
Map Value:2
Map Value:4

由於我們正在更新 myMap 中現有的鍵值,它的大小沒有更改,所以我們沒有收到 ConcurrentModificationException。輸出在你的系統中可能會不同,因為 HashMap 的 keyset 不像列表那樣有序。如果你取消註釋我正在向 HashMap 添加新鍵值的語句,它將導致 ConcurrentModificationException。

為了避免在多線程環境中出現 ConcurrentModificationException

  1. 你可以將列表轉換為數組,然後對數組進行迭代。這種方法適用於小型或中型列表,但如果列表很大,它將嚴重影響性能。
  2. 你可以通过将列表放入同步块来在迭代时锁定它。这种方法并不推荐,因为它会取消多线程的好处。
  3. 如果你使用的是JDK1.5或更高版本,那么你可以使用ConcurrentHashMapCopyOnWriteArrayList类。这是推荐的方法,可以避免并发修改异常。

在单线程环境中避免ConcurrentModificationException

你可以使用迭代器的remove()函数从底层集合对象中删除对象。但在这种情况下,你只能从列表中删除相同的对象,而不能删除其他对象。让我们运行一个使用并发集合类的示例。

package com.journaldev.ConcurrentModificationException;

import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

public class AvoidConcurrentModificationException {

	public static void main(String[] args) {

		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();
			System.out.println("List Value:" + value);
			if (value.equals("3")) {
				myList.remove("4");
				myList.add("6");
				myList.add("7");
			}
		}
		System.out.println("List Size:" + myList.size());

		Map<String, String> myMap = new ConcurrentHashMap<String, String>();
		myMap.put("1", "1");
		myMap.put("2", "2");
		myMap.put("3", "3");

		Iterator<String> it1 = myMap.keySet().iterator();
		while (it1.hasNext()) {
			String key = it1.next();
			System.out.println("Map Value:" + myMap.get(key));
			if (key.equals("1")) {
				myMap.remove("3");
				myMap.put("4", "4");
				myMap.put("5", "5");
			}
		}

		System.out.println("Map Size:" + myMap.size());
	}

}

上面程序的输出如下所示。你可以看到程序没有抛出ConcurrentModificationException。

List Value:1
List Value:2
List Value:3
List Value:4
List Value:5
List Size:6
Map Value:1
Map Value:2
Map Value:4
Map Value:5
Map Size:4

从上面的例子中可以明显看出:

  1. 并发集合类可以安全地进行修改,它们不会抛出ConcurrentModificationException。

  2. 在CopyOnWriteArrayList的情况下,迭代器不会适应列表的更改,而是在原始列表上工作。

  3. 在ConcurrentHashMap的情况下,行为并不总是相同的。对于条件:

    if(key.equals("1")){
    	myMap.remove("3");}
    

    输出结果为:

    Map Value:1
    Map Value:null
    Map Value:4
    Map Value:2
    Map Size:4
    

    它会获取使用键“4”添加的新对象,但不会获取使用键“5”添加的下一个对象。现在,如果我将条件更改为下面的内容。

    if(key.equals("3")){
    	myMap.remove("2");}
    

    输出结果为:

    Map Value:1
    Map Value:3
    Map Value:null
    Map Size:4
    

    在这种情况下,它不会考虑新添加的对象。因此,如果您使用ConcurrentHashMap,请避免添加新对象,因为它可能会根据键集进行处理。请注意,同一程序在您的系统中可能会打印出不同的值,因为HashMap的键集是无序的。

使用for循環來避免java.util.ConcurrentModificationException

如果您正在單線程環境中工作,並且希望代碼能夠處理列表中添加的額外對象,則可以使用for循環而不是Iterator

for(int i = 0; i<myList.size(); i++){
	System.out.println(myList.get(i));
	if(myList.get(i).equals("3")){
		myList.remove(i);
		i--;
		myList.add("6");
	}
}

請注意,我正在減少計數器,因為我正在刪除相同的對象,如果您需要刪除下一個或更遠的對象,則無需減少計數器。試試看。 🙂 還有一件事:如果您嘗試使用subList修改原始列表的結構,則會收到ConcurrentModificationException。讓我們通過一個簡單的例子來看看這一點。

package com.journaldev.ConcurrentModificationException;

import java.util.ArrayList;
import java.util.List;

public class ConcurrentModificationExceptionWithArrayListSubList {

	public static void main(String[] args) {

		List names = new ArrayList<>();
		names.add("Java");
		names.add("PHP");
		names.add("SQL");
		names.add("Angular 2");

		List first2Names = names.subList(0, 2);

		System.out.println(names + " , " + first2Names);

		names.set(1, "JavaScript");
		// 檢查下面的輸出。 :)
		System.out.println(names + " , " + first2Names);

		// 讓我們修改列表大小並獲得ConcurrentModificationException
		names.add("NodeJS");
		System.out.println(names + " , " + first2Names); // this line throws exception

	}

}

上面程序的輸出是:

[Java, PHP, SQL, Angular 2] , [Java, PHP]
[Java, JavaScript, SQL, Angular 2] , [Java, JavaScript]
Exception in thread "main" java.util.ConcurrentModificationException
	at java.base/java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1282)
	at java.base/java.util.ArrayList$SubList.listIterator(ArrayList.java:1151)
	at java.base/java.util.AbstractList.listIterator(AbstractList.java:311)
	at java.base/java.util.ArrayList$SubList.iterator(ArrayList.java:1147)
	at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:465)
	at java.base/java.lang.String.valueOf(String.java:2801)
	at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
	at com.journaldev.ConcurrentModificationException.ConcurrentModificationExceptionWithArrayListSubList.main(ConcurrentModificationExceptionWithArrayListSubList.java:26)

根據ArrayList subList文檔,結構修改僅允許在subList方法返回的列表上進行。返回列表上的所有方法首先檢查支持列表的實際modCount是否等於其期望值,如果不等則拋出ConcurrentModificationException。

你可以從我們的GitHub存儲庫下載所有示例代碼。

Source:
https://www.digitalocean.com/community/tutorials/java-util-concurrentmodificationexception