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类中,那里定义了一个整数变量modCount。modCount提供了列表大小已更改的次数。在每次next()调用中,modCount的值都被用来检查函数checkForComodification()
中是否有任何修改。现在,注释掉列表部分,然后再次运行程序。你会发现现在不会抛出ConcurrentModificationException了。输出:
Map Value:3
Map Value:2
Map Value:4
由于我们正在更新myMap中现有的键值,它的大小没有改变,所以我们没有收到ConcurrentModificationException。在你的系统中,输出可能会有所不同,因为HashMap的keyset不像List那样有序。如果你取消对我在HashMap中添加新键值的语句的注释,它会导致ConcurrentModificationException。
为了避免在多线程环境中出现ConcurrentModificationException
- 您可以将列表转换为数组,然后在数组上进行迭代。这种方法对于小型或中型列表效果很好,但如果列表很大,它会严重影响性能。
- 您可以通过将列表放入同步块中来在迭代时对其进行锁定。这种方法并不推荐,因为它会取消多线程的好处。
- 如果您使用的是JDK1.5或更高版本,则可以使用ConcurrentHashMap和CopyOnWriteArrayList类。这是推荐的方法,以避免并发修改异常。
在单线程环境中避免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
从上面的示例中可以清楚地看出:
-
并发集合类可以安全地进行修改,它们不会抛出ConcurrentModificationException。
-
在CopyOnWriteArrayList的情况下,迭代器不会适应列表中的更改,并且会使用原始列表进行操作。
-
在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循环而不是迭代器。
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方法返回的列表上进行。返回的列表上的所有方法首先检查支持列表的实际modCount是否等于其预期值,如果不等,则抛出ConcurrentModificationException。
Source:
https://www.digitalocean.com/community/tutorials/java-util-concurrentmodificationexception