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

在开发的过程中,我们经常会对集合中的元素进行操作,改变集合的内容可能会产生ConcurrentModificationException异常,本文对该异常进行详细的分析。

可能一些同学看到过fail-fast或者fail-safe的概念,如果不了解的可以点击:
fail-fast(快速失败)和fail-safe(安全失败)

ConcurrentModificationException异常分析

先来看一段代码:

import java.util.ArrayList;
import java.util.List;

/**
 * @author Huangqing
 * @date 2018/7/25 16:37
 */
public class IteratorTest {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("zhangsan");
        list.add("lisi");
        list.add("wangwu");
        list.add("goudan");
        list.add("mafei");
        list.add("lubenwei");
        for (String s : list) {
            if (s.equals("lisi")) {
                list.remove(s);
            }
        }
    }
}

执行结果:
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.hq.iteratorTest.IteratorTest.main(IteratorTest.java:19)

上面代码主要是对删除集合中的某一元素,很多刚接触Java的同学在学习集合的时候,了解到remove(obj)方法,通过该方法删除掉集合中的元素。

我们先来看一下remove(obj)方法的源码(以ArrayList为例)

public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }

存在的疑惑:源码中并没有看到有抛出ConcurrentModificationException异常的代码,可是为什么会抛出此异常呢?

书上是这样说的,编译器在看到一个实现了Interator接口的对象,当该集合对象在使用增强for循环时,会自动地重写,变成使用迭代器来遍历集合

所以开头的代码,相当于以下代码:

public class IteratorTest {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("zhangsan");
        list.add("lisi");
        list.add("wangwu");
        list.add("goudan");
        list.add("mafei");
        list.add("lubenwei");
        Iterator it = list.iterator();
        while (it.hasNext()){
            String s = it.next();
            if (s.equals("lisi")){
                list.remove(s); //注意这里调用的是集合的方法
            }
        }
    }
}

虽然使用了迭代器进行遍历,但执行的remove()方法还是集合对象来操作。

紧接着我们带着问题去寻找答案:通常我们会使用迭代器的remove()方法对集合元素进行操作,这是为什么?

首先我们先看迭代器中的remove()方法源码:

 public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                // 游标
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
 }

ArrayList自带的remove方法源码:

public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

异常检测源码:

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

由以上三段代码和对比前面集合的remove()方法可得:

// modCount  修改次数
// expectedModCount 期望修改次数

在集合中进行操作时,当modCount != expectedModCount时会抛出修改异常。通过源码可以知道,集合在增加,删除元素时都会修改modCount的值,当在集合中删除时,modCount+1,而expectedModCount未改变,而在集合删除完之后,迭代器指向下一个对象(即调用next()方法),会检测出不一致而抛出异常。

迭代器next()源码如下:

public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
}

迭代器的remove方法与集合的remove方法,最大的不同是,迭代器的remove方法中包括对游标和expectedModCount的修正。
因为Iterator是在一个独立的线程中工作的,它在new Itr()进行初始化时,会记录当时集合中的元素,可以理解为记录了集合的状态,在使用集合的Remove方法对集合进行修改时,被记录的集合状态并不会与之同步改变,所以在cursor指向下一个要返回的元素时,可能会发生找不到的错误,即抛出ConcurrentModificationException异常。

很明显,如果使用迭代器提供的remove方法时,会对cursor进行修正,故不会出现错误,此外,还会修正expectedModCount,通过它来进行错误检测(迭代过程中,不允许集合的add,remove,clear等改变集合结构的操作)。

单线程下解决方法

既然知道了出现异常的关键为 modCount 和 expectedModCount的值,我们该如何解决该问题呢?

上文中已经提到的,迭代器的remove方法中,有一行代码 expectedModCount = modCount; 可以保证在修改之后两个变量的值相等。

所以,将之前的代码更正为下面的代码:

 public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("zhangsan");
        list.add("lisi");
        list.add("wangwu");
        list.add("goudan");
        list.add("mafei");
        list.add("lubenwei");
         while (it.hasNext()){
            String s = it.next();
            if (s.equals("lisi")){
                it.remove(); // 注意这里
            }
        }
    }

多线程下解决方法

上面我们已经提供了解决的方案,但是就适用于所有情况了吗?

先看以下一段代码:

/**
 * @author Huangqing
 * @date 2018/7/25 16:37
 */
public class IteratorTest {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("zhangsan");
        list.add("lisi");
        list.add("wangwu");
        list.add("goudan");
        list.add("mafei");
        list.add("lubenwei");
        Thread thread1 = new Thread(){
            @Override
            public void run() {
                Iterator<String> iterator = list.iterator();
                while(iterator.hasNext()){
                    String str = iterator.next();
                    System.out.println(str);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
        };
        Thread thread2 = new Thread(){
            @Override
            public void run() {
                Iterator<String> iterator = list.iterator();
                while(iterator.hasNext()){
                    String str = iterator.next();
                    if (str.equals("lisi")) {
                        iterator.remove();
                    }
                }
            };
        };
        thread1.start();
        thread2.start();
    }
}

这里写图片描述

同样的即使我们使用的是迭代器中的remove方法,在多线程情况下,依旧可能会出现异常?

我们来分析一下出现异常的原因:

当线程A执行遍历的第一次时,正常的打印出集合元素,线程B也正常的执行,我们无法控制CPU的调度,所以运用线程等待的方式,让第二个线程稍快与第一个线程,以检测出异常。当线程A等待的时候,线程B调用remove方法,此时modCount 值已经自增,而未执行到expectedModCount = modCount的代码,此时expectedModCount != modCount,这个时候线程A等待结束,进行第二次循环,当执行String str = iterator.next();时,会进行异常检测,此时因为expectedModCount != modCount而抛出异常。

这就很明显的解释了为什么在多线程情况下也是不安全的。

那么有什么好的解决办法呢?

我们都知道继承了AbstractList的有ArrayList和Vector,而且我们知道ArrayList是非线程安全的,而Vector是线程安全的,有一些朋友说我们可以使用Vector来使得在多线程下操作集合不会产生异常。

其实这里使用Vector依然会出现问题。

/**
 * @author Huangqing
 * @date 2018/7/25 16:37
 */
public class IteratorTest {
    public static void main(String[] args) {
        Vector<String> list = new Vector<>();
        list.add("zhangsan");
        list.add("lisi");
        list.add("wangwu");
        list.add("goudan");
        list.add("mafei");
        list.add("lubenwei");
        Thread thread1 = new Thread(){
            @Override
            public void run() {
                Iterator<String> iterator = list.iterator();
                while(iterator.hasNext()){
                    String str = iterator.next();
                    System.out.println(str);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
        };
        Thread thread2 = new Thread(){
            @Override
            public void run() {
                Iterator<String> iterator = list.iterator();
                while(iterator.hasNext()){
                    String str = iterator.next();
                    if (str.equals("lisi")) {
                        iterator.remove();
                    }
                }
            };
        };
        thread1.start();
        thread2.start();
    }
}

这里写图片描述

通过例子我们可以知道,其实Vector算不上是一个线程安全的集合类,至于为什么说他是线程安全的,我也不太清楚,可能是因为Vector很多方法上采用了synchronized的同步关键字,但是请注意:同步 != 线程安全(该概念不在本章讨论)

当我们使用Vector时,虽然在对集合的操作上同步,但是别忘了我们使用的是集合中的迭代器,也就是上文说的,当我们在使用循环的使用,只要这个集合包含了迭代器类,那么就会使用迭代器进行循环,所以每个线程的迭代器还是线程私有的,而 modCount是共享的,这里同样会出现 modCount != expectedModCount 的情况,所以会产生异常情况。

解决方法:

1)在使用iterator迭代的时候使用synchronized或者Lock进行同步;
2)使用并发容器CopyOnWriteArrayList代替ArrayList和Vector。

感谢观看!

猜你喜欢

转载自blog.csdn.net/qq_20492999/article/details/81216453
今日推荐