List集合循环remove/add时报错ConcurrentModificationException

在这里插入图片描述
其实关于这个问题,在阿里巴巴Java开发手册中有规定:
在这里插入图片描述
WHY ?

查看源码之后才知道是因为
在这里插入图片描述

那么modCount和expectedModCount是什么呢?
在这里插入图片描述

modCount是ArrayList中的一个成员变量。它表示该集合实际被修改的次数。
expectedModCount 是 ArrayList中的一个内部类——Itr中的成员变量。

expectedModCount表示这个迭代器预期该集合被修改的次数。其值随着Itr被创建而初始化。只有通过迭代器对集合进行操作,该值才会改变。

那么,接着我们看下userNames.remove(userName);方法里面做了什么事情,为什么会导致expectedModCount和modCount的值不一样。

通过翻阅代码,我们也可以发现,remove方法核心逻辑如下:
在这里插入图片描述
可以看到,remove方法只修改了modCount,并没有对expectedModCount做任何操作。

之所以会抛出CMException异常,是因为我们的代码中使用了增强for循环,而在增强for循环中,集合遍历是通过iterator进行的,但是元素的add/remove却是直接使用的集合类自己的方法。这就导致iterator在遍历的时候,会发现有一个元素在自己不知不觉的情况下就被删除/添加了,就会抛出一个异常,用来提示用户,可能发生了并发修改!
所以,在使用Java的集合类的时候,如果发生CMException,优先考虑fail-fast有关的情况,实际上这里并没有真的发生并发,只是Iterator使用了fail-fast的保护机制,只要他发现有某一次修改是未经过自己进行的,那么就会抛出异常

使用CopyOnWriteArrayList代替了ArrayList,就不会发生异常。

fail-safe集合的所有对集合的修改都是先拷贝一份副本,然后在副本集合上进行的,并不是直接对原集合进行修改。并且这些修改方法,如add/remove都是通过加锁来控制并发的。

所以,CopyOnWriteArrayList中的迭代器在迭代的过程中不需要做fail-fast的并发检测。(因为fail-fast的主要目的就是识别并发,然后通过异常的方式通知用户)

但是,虽然基于拷贝内容的优点是避免了ConcurrentModificationException,但同样地,迭代器并不能访问到修改后的内容。如以下代码:

public static void main(String[] args) {
    List<String> userNames = new CopyOnWriteArrayList<String>() {{
        add("Hollis");
        add("hollis");
        add("HollisChuang");
        add("H");
    }};

    Iterator it = userNames.iterator();

    for (String userName : userNames) {
        if (userName.equals("Hollis")) {
            userNames.remove(userName);
        }
    }

    System.out.println(userNames);

    while(it.hasNext()){
        System.out.println(it.next());
    }
}

我们得到CopyOnWriteArrayList的Iterator之后,通过for循环直接删除原数组中的值,最后在结尾处输出Iterator,结果发现内容如下:

[hollis, HollisChuang, H]
Hollis
hollis
HollisChuang
H

迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。
其他方式

  • 使用迭代器(Iterator)+while循环,
  • 普通fori循环,
  • 使用Java 8中提供的filter过滤(重新生成一个集合)
  • 使用增强for循环其实也可以(找到这个元素删除后 break推出循环)
  • 使用fail-safe的集合类(ConcurrentLinkedDeque)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章