java.util.ConcurrentModificationException
使用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。