并发修改异常(ConcurrentModificationException)

253 阅读3分钟

下面通过一段简单的代码复现开发需求过程中遇到的问题:

public class Test {

    private static Map<String, List<String>> LIST_MAP = new HashMap<>();

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(() -> {
                List<String> list = fetchList();
                Collections.sort(list);
                for (String e : list) {
                    System.out.println(e);
                }
            });
            thread.start();
        }
    }

    private static List<String> fetchList() {
        if (!CollectionUtils.isEmpty(LIST_MAP)) {
            return LIST_MAP.get("key");
        }
        refreshListMap();
        return LIST_MAP.get("key");
    }

    private static void refreshListMap(){
        ArrayList<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        LIST_MAP.put("key", list);
    }
}

代码运行时,第10行和第11行交替报出异常,具体异常为ConcurrentModificationException。看下代码逻辑,其实就是先获取到一个集合,对集合先进行一次排序,在遍历集合。可是为啥会报并发修改异常,有以下几个疑问点:

  1. Collections.sort()方法对集合进行排序,为啥会出现并发修改的异常?
  2. 通过增强for循环遍历集合,并没有去修改集合的内部结构,为啥会出现并发修改的异常?

通常对于这种异常,个人喜欢去看源码,结合源码分析效果会更高。依次看下【增强for循环】以及【Collections.sort()】各自的内部实现

增强for循环

  • 增强for循环内部原理其实是一个 Iterator 迭代器
  • 迭代器主要用于遍历Collection集合中的元素,即所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象

首先我们进入 AbstractList的iterator() 方法 , 这里iterator是返回了了一个新对象Itr

public Iterator<E> iterator() {
    return new Itr();
}

Itr是AbstractList的一个内部类,来看一下Itr这个类的具体内容


private class Itr implements Iterator<E> {
    /**
     * Index of element to be returned by subsequent call to next.
     */
    int cursor = 0;

    /**
     * Index of element returned by most recent call to next or
     * previous.  Reset to -1 if this element is deleted by a call
     * to remove.
     */
    int lastRet = -1;

    /**
     * The modCount value that the iterator believes that the backing
     * List should have.  If this expectation is violated, the iterator
     * has detected concurrent modification.
     */
    int expectedModCount = modCount;

    public boolean hasNext() {
        return cursor != size();
    }

    public E next() {
        checkForComodification();
        try {
            int i = cursor;
            E next = get(i);
            lastRet = i;
            cursor = i + 1;
            return next;
        } catch (IndexOutOfBoundsException e) {
            checkForComodification();
            throw new NoSuchElementException();
        }
    }

关注下【int expectedModCount = modCount】,其中modCount代表对集合的实际操作次数 expectedModCount代表对集合的预期操作次数,初始时expectedModCount=modCount,后续的检查都是基于这两个数值。注意到迭代器中的next方法,在方法的一开始执行了checkForComodification方法,方法内容如下:

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

这个方法会抛出ConcurrentModificationException异常,前提是modCount != expectedModCount,那也就是在遍历集合的时候,线程中执行的其他方法修改了modCount或者expectedModCount的值

Collections.sort()

介绍完了【增强for循环】,下面再说下集合的sort方法,sort方法的实现如下:

public void sort(Comparator<? super E> c) {
    final int expectedModCount = modCount;
    Arrays.sort((E[]) elementData, 0, size, c);
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
    modCount++;
}

同样的,在对集合排序时,也会校验modCount和expectedModCount,不相同的话也会抛出ConcurrentModificationException异常。但这个都不是重点,重点是sort方法的最后一步是modCount++,修改了modCount的值

原先以为只有对集合进行如add、‌remove等方法才会导致modCount增加,没想到对集合进行sort、clear也会导致modCount++,看来只要改变了集合中的元素个数、元素顺序都算对集合进行了结构性修改

看了【增强for循环】以及【Collections.sort()】两者的内部实现,问题就迎刃而解了。

  1. 原因一:这两步操作都会判断集合的修改次数
  2. 原因二:sort完集合后,集合的修改次数会自增1
  3. 原因三:fetchList方法返回的集合都是同一个对象

综合以上三个原因,多线程并发执行的情况下,就会交替抛出ConcurrentModificationException异常。具体的解决方案如下:

  1. 每个线程都new一个集合出来,保证多个线程不是操作同一个集合,同一个集合的话,维护的modCount和expectedModCount也都是同一份
  2. 不直接对集合本身进行排序,例如通过stream流对集合进行排序,返回一个排序好的新集合,避免对集合进行操作后导致modCount和expectedModCount被修改