java.util.ConcurrentModificationException

时间:2020-02-23 14:34:35  来源:igfitidea点击:

使用Java集合类时,java.util.ConcurrentModificationException是一个非常常见的异常。
Java Collection类是快速失败的,这意味着如果某个线程正在使用迭代器遍历Collection时更改Collection,则iterator.next()将抛出ConcurrentModificationException。

在多线程以及单线程Java编程环境中,可能会发生并发修改异常。

java.util.ConcurrentModificationException

让我们看一个并发修改异常场景的例子。

package com.theitroad.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<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();
			System.out.println("List Value:" + value);
			if (value.equals("3"))
				myList.remove(value);
		}

		Map<String, String> myMap = new HashMap<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("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.theitroad.ConcurrentModificationException.ConcurrentModificationExceptionExample.main(ConcurrentModificationExceptionExample.java:22)

从输出堆栈跟踪中可以明显看出,当我们调用iteratornext()函数时,会引发并发修改异常。

如果您想知道Iterator如何检查修改,它的实现可以在AbstractList类中找到,该类定义了一个int变量modCount。
modCount提供更改列表大小的次数。
在每个next()调用中都使用modCount值来检查函数checkForComodification()中是否有任何修改。

现在,注释掉列表部分,然后再次运行该程序。
您将看到现在没有抛出ConcurrentModificationException。

输出:

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

由于我们正在更新myMap中的现有键值,因此其大小没有更改,并且没有收到ConcurrentModificationException。
在您的系统中,输出可能会有所不同,因为HashMap键集的排序方式与列表不同。

如果您将在HashMap中添加新键值的语句取消注释,则将导致ConcurrentModificationException。

在多线程环境中避免ConcurrentModificationException

  • 您可以将列表转换为数组,然后在数组上进行迭代。
    这种方法适用于中小型列表,但是如果列表很大,则对性能的影响很大。

  • 您可以通过将列表放在同步块中来在锁定时锁定列表。
    不建议使用此方法,因为它将停止多线程的好处。

  • 如果使用的是JDK1.5或者更高版本,则可以使用ConcurrentHashMap和CopyOnWriteArrayList类。
    建议使用此方法来避免并发修改异常。

在单线程环境中避免ConcurrentModificationException

您可以使用迭代器的remove()函数从基础集合对象中删除该对象。
但是在这种情况下,您可以从列表中删除同一对象,而不能删除任何其他对象。

让我们使用并发集合类运行示例。

package com.theitroad.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

从以上示例可以清楚地看出:

  • 可以安全地修改并发Collection类,它们不会引发ConcurrentModificationException。

  • 如果使用CopyOnWriteArrayList,则迭代器无法容纳列表中的更改,并且可以处理原始列表。

  • 对于ConcurrentHashMap,行为并不总是相同的。

输出为:
它正在使用添加了键" 4"的新对象,而不是下一个添加了键" 5"的对象。
现在,如果将条件更改为以下内容。

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

使用for循环避免java.util.ConcurrentModificationException

如果您正在单线程环境中工作,并且希望您的代码处理列表中添加的另外对象,则可以使用for循环而不是Iterator。

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

请注意,我要减少计数器是因为我要删除同一对象,如果必须删除下一个或者更远的对象,则无需减少计数器。

另一件事:如果尝试使用subList修改原始列表的结构,则将获得ConcurrentModificationException。
让我们用一个简单的例子来看一下。

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

上面程序的输出是:

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

根据ArrayList subList文档,仅对subList方法返回的列表进行结构修改。
返回列表上的所有方法首先检查后备列表的实际modCount是否等于其期望值,如果不是,则抛出ConcurrentModificationException。