java.util.ConcurrentModificationException
java.util.ConcurrentModificationException 是使用 Java 集合类时非常常见的异常。Java 集合类是快速失败的,这意味着如果某个线程使用迭代器遍历集合时集合发生更改,则会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<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.journaldev.ConcurrentModificationException.ConcurrentModificationExceptionExample.main(ConcurrentModificationExceptionExample.java:22)
从输出堆栈跟踪中可以清楚地看出,当我们调用迭代器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()
函数从底层集合对象中删除对象。但在这种情况下,您可以删除同一个对象,而不是列表中的任何其他对象。让我们使用并发集合类运行一个示例。
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
从上面的例子可以清楚地看出:
-
Concurrent Collection 类可以安全地修改,它们不会抛出 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 循环而不是 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<String> names = new ArrayList<>();
names.add("Java");
names.add("PHP");
names.add("SQL");
names.add("Angular 2");
List<String> first2Names = names.subList(0, 2);
System.out.println(names + " , " + first2Names);
names.set(1, "JavaScript");
// check the output below. :)
System.out.println(names + " , " + first2Names);
// Let's modify the list size and get 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 存储库下载所有示例代码。