解决:java.util.ConcurrentModificationException

解决:java.util.ConcurrentModificationException

Symon 1,861 2020-10-26

ConcurrentModificationException

在使用Java集合类时,java.util.ConcurrentModificationException是一个非常常见的异常。fail-fast机制是Java集合的一种错误机制。如果某个线程在迭代元素时,集合的结构发生了改变,此时iterator.next()就会抛出ConcurrentModificationException异常。

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

下面是个常见的例子:

public static void main(String args[]) {
    List<String> demoList = new ArrayList<>();
    demoList.add("1");
    demoList.add("2");
    demoList.add("3");
    demoList.add("4");
    demoList.add("5");
    Iterator<String> it = demoList.iterator();
    while (it.hasNext()) {
        String value = it.next();
        System.out.println("List Value:" + value);
        if (value.equals("3"))
            demoList.remove(value);
    }

    /*Map<String, String> demoMap = new HashMap<>();
    demoMap.put("1", "1");
    demoMap.put("2", "2");
    demoMap.put("3", "3");
    Iterator<String> mapIt = demoMap.keySet().iterator();
    while (mapIt.hasNext()) {
        String key = mapIt.next();
        System.out.println("Map Value:" + demoMap.get(key));
        if (key.equals("2")) {
            //demoMap.put("1", "4");
            demoMap.put("4", "4");
        }
    }*/
}

以上例子在执行时就会抛出java.util.ConcurrentModificationException异常,控制台日志如下:

List Value:1
List Value:2
List Value:3
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at com.example.demo.study.exception.ConcurrentModificationExceptionDemo.main(ConcurrentModificationExceptionDemo.java:21)

从日志信息中可以看出,当程序迭代器调用next()函数时,抛出ConcurrentModificationException异常。
通过跟踪源码可以发现,每次调用next()函数前,都会调用checkForComodification()来检查变量modCount是否被修改。

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

在多线程环境中避免ConcurrentModificationException

1.将列表转换为数组,然后在数组上迭代。适用于小型列表,如果是列表很大,将会对性能产生很大影响。
2.将列表放在同步块中,来锁定列表。不建议使用,违背了多线程的宗旨。
3.JDK1.5或更高版本,可使用ConcurrentHashMapCopyOnWriteArrayList并发集合类。建议使用此方法来避免ConcurrentModificationException

在单线程环境中避免ConcurrentModificationException

可以使用Iteratorremove()函数,从列表中删除当前对象。

下面是使用并发集合类的例子:

public static void main(String args[]) {
    List<String> demoList = new CopyOnWriteArrayList<>();
    demoList.add("1");
    demoList.add("2");
    demoList.add("3");
    demoList.add("4");
    demoList.add("5");
    Iterator<String> it = demoList.iterator();
    while (it.hasNext()) {
        String value = it.next();
        System.out.println("demoList Value:" + value);
        if (value.equals("2")) {
            demoList.remove("4");
            demoList.add("6");
            demoList.add("7");
        }
    }
    System.out.println("demoList :" + demoList);
    System.out.println("demoList Size:" + demoList.size());

    Map<String, String> demoMap = new ConcurrentHashMap<>();
    demoMap.put("1", "1");
    demoMap.put("2", "2");
    demoMap.put("3", "3");
    Iterator<String> mapIt = demoMap.keySet().iterator();
    while (mapIt.hasNext()) {
        String key = mapIt.next();
        System.out.println("demoMap Value:" + demoMap.get(key));
        if (key.equals("2")) {
            demoMap.remove("3");
            demoMap.put("4", "4");
            demoMap.put("5", "5");
        }
    }
    System.out.println("demoList :" + demoMap);
    System.out.println("demoMap Size:" + demoMap.size());
}

运行结果如下:并没有抛出ConcurrentModificationException异常。

demoList Value:1
demoList Value:2
demoList Value:3
demoList Value:4
demoList Value:5
demoList :[1, 2, 3, 5, 6, 7]
demoList Size:6
demoMap Value:1
demoMap Value:2
demoMap Value:null
demoMap Value:4
demoMap Value:5
demoList :{1=1, 2=2, 4=4, 5=5}
demoMap Size:4

使用for循环避免ConcurrentModificationException

如果是单线程环境中,需要在处理列表时添加额外对象,希望使用for循环而不是Iterator来实现。

for (int i = 0; i < demoList.size(); i++) {
    System.out.println(demoList.get(i));
    if (demoList.get(i).equals("3")) {
        demoList.remove(i);
        i--;
        demoList.add("6");
    }
}

注意,由于要删除的是同一对象,所以计数器要减1,如果其他的对象,则不需要计数器减1。自己尝试。

需要注意的一点
当使用subList的时修改了原始列表的结构,那么将会抛出ConcurrentModificationException

public static void main(String args[]) {
        List<String> stores = new ArrayList<>();
        stores.add("A001");
        stores.add("B001");
        stores.add("C001");
        stores.add("D001");

        List<String> first2Stores = stores.subList(0, 2);
        System.out.println(stores + " , " + first2Stores);
        stores.set(1, "B002");
        System.out.println(stores + " , " + first2Stores);
        stores.add("E001");
        System.out.println(stores + " , " + first2Stores);//这里抛出异常
    }

输出结果如下:

[A001, B001, C001, D001] , [A001, B001]
[A001, B002, C001, D001] , [A001, B002]
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1239)
	at java.util.ArrayList$SubList.listIterator(ArrayList.java:1099)
	at java.util.AbstractList.listIterator(AbstractList.java:299)
	at java.util.ArrayList$SubList.iterator(ArrayList.java:1095)
	at java.util.AbstractCollection.toString(AbstractCollection.java:454)
	at java.lang.String.valueOf(String.java:2994)
	at java.lang.StringBuilder.append(StringBuilder.java:131)
	at com.example.demo.study.exception.ConcurrentModificationExceptionDemo.main(ConcurrentModificationExceptionDemo.java:102)

查看ArrayListsubList的源码文档就能知道,除了允许subList返回的列表结构发生改变,其他任何方法首先都会检查源列表的modCount是否等于期望值,如果不是,则抛出ConcurrentModificationException


# java # exception