在使用Java集合类时,java.util.ConcurrentModificationException
是一个非常常见的异常。Java Collection
类是快速失败的,如果在某个线程使用迭代器遍历它时将更改Collection,则iterator.next()
将抛出ConcurrentModificationException
异常。多线程以及单线程java编程环境可能会出现并发修改异常。
让我们看一个示例的并发修引发改异常场景。
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.yiibai.ConcurrentModificationException.ConcurrentModificationExceptionExample.main(ConcurrentModificationExceptionExample.java:22)
从输出堆栈跟踪中可以看出,当调用iterator next()
函数时,并发修改异常将会引发。Iterator
如何检查修改?它的实现存在AbstractList
类中,其中定义了一个int
类型变量modCount
。modCount
提供列表大小已更改的次数。modCount
值用于每个next()
调用,以检查函数checkForComodification()
中的任何修改。
现在注释掉列表部分并再次运行程序。现在没有抛出ConcurrentModificationException
异常。
输出结果为:
Map Value:3
Map Value:2
Map Value:4
由于更新myMap
中的键值,因此它的大小尚未更改,并且没有引发ConcurrentModificationException
异常。请注意,系统中的输出可能不同,因为HashMap
键集不像List
那样排序。如果要取消注释在HashMap
中添加新键值的语句,它将引发ConcurrentModificationException
异常。
在多线程环境中避免ConcurrentModificationException异常
- 可以将列表转换为数组,然后迭代数组。这种方法适用于中小型列表,但如果列表很大,则会对性能产生很大影响。
- 可以在迭代时锁定列表,方法是将其放入同步块中。不推荐这种方法,因为它不会带来多线程的好处。
- 如果使用的是JDK1.5或更高版本,则可以使用
ConcurrentHashMap
和CopyOnWriteArrayList
类。这是避免并发修改异常的推荐方法。
在单线程环境中避免ConcurrentModificationException
可以使用迭代器remove()
函数从底层集合对象中删除该对象。但在这种情况下,从列表中删除的对象而不删除其他对象。
下面示例中使用Collection
类:
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.OnWriteArrayList;
public class AvoidConcurrentModificationException {
public static void main(String[] args) {
List<String> myList = new OnWriteArrayList<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
从上面的例子可以清楚地看出:
可以安全地修改并发集合类,它们不会抛出ConcurrentModificationException
异常。
对于CopyOnWriteArrayList
类,迭代器不适应列表中的更改并适用于原始列表。
在ConcurrentHashMap
的情况下,行为并不总是相同。
条件:
f(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
键集进行排序。