java.util.ConcurrentModificationException异常解决和分析

一、异常再现及解决方案 

在遍历ArrayList并对其进行删除操作时,跳出java.util.ConcurrentModificationException异常,先上错误代码:

    
    @Test
    public void test1() {
        List<String> list = new ArrayList<String>();
        list.add("小白");
        list.add("小紫");
        list.add("小红");
        list.add("小绿");
        list.add("小兰");
        for (String str : list) {
            if(str.equals("小紫")){
                list.remove(str);
            }
        }
    }

    @Test
    public void test2() {
        List<String> list = new ArrayList<String>();
        list.add("小白");
        list.add("小紫");
        list.add("小红");
        list.add("小绿");
        list.add("小兰");
        Iterator<String> it = list.iterator();
        while(it.hasNext()){
            String x = it.next();
            if(x.equals("小紫")){
                list.remove(x);
            }
        }
    }

我出现的是test1的问题,还有一种就是test2也会有这样的问题。

其实不只是遍历ArrayList会出现 ConcurrentModificationException 异常,只要是集合 Collection 的子类都会出现这个问题。

正确的用法如下:

    @Test
    public void test6() {
        List<String> list = new ArrayList<String>();
        list.add("小白");
        list.add("小紫");
        list.add("小红");
        list.add("小绿");
        list.add("小兰");
        Iterator<String> it = list.iterator();
        while(it.hasNext()){
            String x = it.next();
            if(x.equals("小紫")){
                it.remove();
            }
        }
    }

循环要用iterator.hasNext();删除元素不能用集合的remove(),而要用迭代器iterator的remove。注意好这两点就不会报 ConcurrentModificationException 常啦!

二、ConcurrentModificationException异常分析

1、test1 分析

    test1使用的是foreach遍历(增强for循环),进入源码中看它的实现如下

        我们可以看到,他抛出异常的是因为 modCount != expectedModCount ,那么需要先弄清楚这两个参数是什么。

        modCount 该参数继承自抽象类AbstractList,在AbstractList中对他的解释如下,直译第一句:结构被修改的次数。也就是ArrayList这个对象被修改的次数。expectedModCount,顾名思义,期望被修改的次数,主要是循环 遍历时用来临时保存modCount的参数,防止对象还在循环遍历的时候被突然修改而导致遍历出错的情况。当遍历过程中出现了unexpectedly(出乎意料地)的操作时,按照 fail-fast (快速失败)原则便会立刻停止并报出 ConcurrentModificationException 异常。

        回到案例分析,在ArrayList执行add和remove操作时(remove方法中会调用fastRemove方法),都会执行modCount++的自增。

        于是乎,在 test1 的案例中add了五个小盆友,所以此时 modCount==5。接下来的foreach遍历操作将 modCount 赋值给了 expectedModCount ,所以它也是5。test1 中循环到‘小紫’的时候我执行了remove操作,因此 modCount 自增变成了6。这时modCount 跟 expectedModCount 就不相等了,于是便报出 ConcurrentModificationException 异常。

2、test2 分析

    两个案例出错的原理差不多。同样看源码,it.next()的实现源码如下

    在 next 中首先会执行 checkForComodification() 方法,去判断 modCount 和 expectedModCount是否相等,不相等就报 ConcurrentModificationException 异常。

      同理,在 test2 中由于循环时进行了 remove 操作,导致 modCount 自增,modCount != expectedModCount,所以就报出了 ConcurrentModificationException 异常。

3、正确用法分析

        正确用法中,主要是使用了 Iterator 迭代器的 remove 方法。

        原理(来源):

         Iterator 是工作在一个独立的线程中,并且拥有一个 mutex 锁。 
        Iterator 被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变。当索引指针往后移动的时候就找不到要迭代的对象,按照 fail-fast 原则 Iterator 会马上抛出 java.util.ConcurrentModificationException 异常。

        所以 Iterator 在工作的时候是不允许被迭代的对象被改变的。但你可以使用 Iterator 本身的方法 remove() 来删除对象, Iterator.remove() 方法会在删除当前迭代对象的同时维护索引的一致性。 

        解释一下就是 Iterator.remove() 方法中多了一个 expectedModCount=modCount,维护了索引的一致性,所以它就不会报 ConcurrentModificationException 异常。

三、拓展

        上面提到的问题和分析都是在单线程的情况下的,接下来讲讲多线程下出现异常的情况,虽然我还没遇到,但既然学到了就将它记录下来,方便日后复习。

1、问题再现

    public static void main(String[] args) {
        test1();
    }

    private static void test1() {
        final List<String> list = new ArrayList<String>();
        list.add("小白");
        list.add("小紫");
        list.add("小红");
        list.add("小绿");
        list.add("小兰");

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                Iterator<String> it = list.iterator();
                while (it.hasNext()) {
                    String str = it.next();
                    System.out.println("Thread1 --- " + str);
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                Iterator<String> it = list.iterator();
                while (it.hasNext()) {
                    String str = it.next();
                    if (str.equals("小紫")) {
                        it.remove();
                    }else {
                        System.out.println("Thread2 --- " + str);
                    }
                }
            }
        });
        thread1.start();
        thread2.start();
         
    }

运行结果:

        案例中主要开启了两个线程,线程1是延时遍历,线程2是遍历+删除操作,注意用的都是同一个 list 集合。结果我们可以看到 thread1 遍历了第一个后进入了 sleep 休眠状态,此时 thread2 开始遍历并送走了‘小紫’(remove后modCount +1),遍历完成后, thread1 休眠完毕,准备遍历第二个元素‘小紫’,但此时小紫已被移除,同时modCount != expectedModCount,所以就会报ConcurrentModificationException 异常。结合表格看可能会更清晰:

时间点arrayList.modCountthread1 iterator.expectedModCountthread2 iterator.expectedModCount
thread start,初始化iterator555
thread2.remove()调用之后656

        两个线程都有各自创建的 Iterator 迭代器,一个线程 remove 后 expectedModCount 被重新赋值,另一个线程的 expectedModCount 是无法了解并同步的,所以 remove 操作后 thread1 的expectedModCount 依然是5,跟 modCount 自然就不相等了。

2、解决方案

(1)方案一:线程加锁

        给每个线程加同步锁,就是在遍历前将 list 锁住,仅执行当前线程的循环遍历操作,其他线程等待。本质上和单线程类似,优点就是不会报异常,缺点就是多变单,效率降低了。

    public static void main(String[] args) {
        test1();
    }

    private static void test1() {
        final List<String> list = new ArrayList<String>();
        list.add("小白");
        list.add("小紫");
        list.add("小红");
        list.add("小绿");
        list.add("小兰");

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (list) {
                    Iterator<String> it = list.iterator();
                    while (it.hasNext()) {
                        String str = it.next();
                        System.out.println("Thread1 --- " + str);
                        try {
                            Thread.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (list) {
                    Iterator<String> it = list.iterator();
                    while (it.hasNext()) {
                        String str = it.next();
                        if (str.equals("小紫")) {
                            it.remove();
                        }else {
                            System.out.println("Thread2 --- " + str);
                        }
                    }
                }
            }
        });
        thread1.start();
        thread2.start();
         
    }

(2)方案二:使用CopyOnWriteArrayList

    public static void main(String[] args) {
        test1();
    }

    private static void test1() {
        final List<String> list = new CopyOnWriteArrayList<String>();
        list.add("小白");
        list.add("小紫");
        list.add("小红");
        list.add("小绿");
        list.add("小兰");

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                Iterator<String> it = list.iterator();
                while (it.hasNext()) {
                    String str = it.next();
                    System.out.println("Thread1 --- " + str);
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                Iterator<String> it = list.iterator();
                while (it.hasNext()) {
                    String str = it.next();
                    if (str.equals("小紫")) {
                        list.remove("小紫");
                    }else {
                        System.out.println("Thread2 --- " + str);
                    }
                }
            }
        });
        thread1.start();
        thread2.start();
         
    }

运行结果:

        可以看到,thread2 已经 remove 了小紫,thread1 仍然能遍历出来,并且没报错。原因的话继续从源码中找,直接看下 CopyOnWriteArrayList 的源码。

        可以看到,在初始化迭代器的时候,list 对象其实已经被复制保存在 private final Object[] snapshot; 这个临时对象里面,接下来的遍历用的都是这个保存复制的对象。所以 thread1 和 thread2 在初始化 Iterator 迭代器时,各自复制保存了 list 对象,操作便互不相干。即使在 thread2 中 remove 掉了“小紫”,thread1 中用的是已经复制保存好的不会受到影响,也就能打印出“小紫”了。

时间点CopyOnWriteArrayList对象thread1thread2
thread.start()ACopy-A1Copy-A2
thread2.remove后BCopy-A1B

        结合表格看可能会好理解些,thread2 使用copy-A在 remove 操作后,会通过 setArray 将修改过后的对象B,赋值回给原来的 array 对象。

        使用 CopyOnWriteArrayList 有几个需要注意的地方。

        1、从 CopyOnWriteArrayList 的源码可以看到,CopyOnWriteArrayList 创建的 Iterator 迭代器是不能 remove、set 和 add 操作的,否则会 throw new UnsupportedOperationException(); 提示你不支持此操作。

        但是呢,是可以用 CopyOnWriteArrayList 自带的 remove 操作的,所以我在案例中没有用迭代器的 remove,用的是 list 自带的 remove。不会报错是因为他每次 remove 操作后会将修改后的array 对象重新赋值给 expectedArray ,这样在遍历下一个时 checkForComodification() 就不会报错了。跟上面的原理差不多,只是这里 modCount  直接换成了 array 对象。

        2、 第二点需要注意的是,thread2对array数组的修改thread1并不能被动感知到,只能通过hashCode()方法去主动感知。

        3、最后一点是效率问题,可以看到整个过程 array 对象拷贝来拷贝去的,每次修改还需要重新 new 一个 array 对象,这样看效率自然就降低了。

参考文章

https://www.cnblogs.com/zhuyeshen/p/10956820.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值