并发修改异常ConcurrentModificationException详解(一)

简介: 并发修改异常ConcurrentModificationException详解

一、简介

在多线程编程中,相信很多小伙伴都遇到过并发修改异常ConcurrentModificationException,本篇文章我们就来讲解并发修改异常的现象以及分析一下它是如何产生的。

  • 异常产生原因:并发修改异常指的是在并发环境下,当方法检测到对象的并发修改,但不允许这种修改时,抛出该异常。

下面看一个示例:


public class TestConcurrentModifyException {
    public static void main(String[] args) {
        List list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            String nextElement = iterator.next();
            if (Integer.parseInt(nextElement) < 2) {
                list.add("2");
            }
        }
    }
}

运行此程序,控制台输出,程序出现异常:


Exception in thread "main" java.util.ConcurrentModificationException
  at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
  at java.util.ArrayList$Itr.next(ArrayList.java:851)
  at com.wsh.springboot.helloworld.TestConcurrentModifyException.main(TestConcurrentModifyException.java:15)

可见,控制台显示的ConcurrentModificationException,即并发修改异常。下面我们就以ArrayList集合中出现的并发修改异常为例来分析异常产生的原因。

二、异常原因分析

通过上面的异常信息可见异常抛出在ArrayList类中的checkForComodification()方法中。下面是checkForComodification方法的源码:


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

checkForComodification()方法实际上就是当modCount 变量值不等于expectedModCount变量值时,就会触发此异常。

那么modCount 和expectedModCount分别代表什么呢?

  • modCount :AbstractList类中的一个成员变量,由于ArrayList继承自AbstractList,所以ArrayList中的modCount变量也继承过来了。


protected transient int modCount = 0;

简单理解,modCount 就是ArrayList中集合结构的修改次数【实际修改次数】,指的是新增、删除(不包括修改)操作。

  • expectedModCount:是ArrayList中内部类Itr的一个成员变量,当我们调用iteroter()获取迭代器方法时,会创建内部类Itr的对象,并给其成员变量expectedModCount赋值为ArrayList对象成员变量的值modCount【预期修改次数】。


private class Itr implements Iterator {
    //游标, 每获取一次元素,游标会向后移动一位
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    //将ArrayList对象成员变量的值modCount赋值给expectedModCount成员变量
    int expectedModCount = modCount;
    //....
    }

经过上面的分析,我们知道了当我们获取到集合的迭代器之后,Itr对象创建成功后,expectedModCount 的值就确定了,就是modCount的值,在迭代期间不允许改变了。要了解它两为啥不相等, 我们就需要观察ArrayList集合的什么操作会导致modCount变量发生变化,从而导致modCount != expectedModCount ,从而发生并发修改异常。

查看ArrayList的源码可知,modCount 初始值为0, 每当集合中添加一个元素或者删除一个元素时,modCount变量的值都会加一,表示集合中结构修改次数多了一次。下面简单看下ArrayList的add()方法和remove()方法。

  • add():每添加一个元素,modCount的值也会自增一次


public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    //第一次添加元素
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        //默认容量DEFAULT_CAPACITY为10
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
    //集合结构修改次数加一
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        //扩容方法,扩容后是原容量的1.5倍
        //扩容前:数组长度10  扩容后:数组长度变为10 + (10 / 2) = 15
        grow(minCapacity);
}
  • remove():每删除一个元素,modCount的值会自增一次


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;
}

**注意!注意!注意!ArrayList中的修改方法set()并不会导致modCount变量发生变化,**set()方法源码如下:


public E set(int index, E element) {
    rangeCheck(index);
    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

三、异常原因追踪

下面我们就Debug调试一下刚刚那个例子,详解了解一下,并发修改异常时怎么产生的。

当我们调用iterator()获取迭代器时,实际上底层创建了一个Itr内部类对象


public Iterator iterator() {
    return new Itr();
}

初始化Itr的成员变量:可以看到,expectedModCount = 3,表示预期修改次数为3,如果在迭代过程中,发现modCount不等于3了,那么就会触发并发修改异常。

image.png

下面简单说明一下Itr的源码:


private class Itr implements Iterator {
    //cursor初始值为0,每次取出一个元素,cursor值会+1,以便下一次能指向下一个元素,直到cursor值等于集合的长度为止
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    //初始化预期修改次数为实际修改次数modCount,即上图中的3
    int expectedModCount = modCount;
    //判断是否还有下一个元素:通过比较游标cursor是否等于数组的长度
    //因为集合中最后一个元素的索引为size-1,只要cursor值不等于size,证明还有下一个元素,此时hasNext方法返回true,
   //如果cursor值与size相等,那么证明已经迭代到最后一个元素,返回false
    public boolean hasNext() {
        return cursor != size;
    }
    //拿出集合中的下一个元素
    @SuppressWarnings("unchecked")
    public E next() {
        //并发修改异常出现根源
        //ConcurrentModificationException异常就是从这抛出的
        //当迭代器通过next()方法返回元素之前都会检查集合中的modCount和最初赋值给迭代器的expectedModCount是否相等,如果不等,则抛出并发修改异常
        checkForComodification();
        int i = cursor;
        //判断,如果大于集合的长度,说明没有元素了。
        if (i >= size)
            throw new NoSuchElementException();
        //将集合存储数据数组的地址赋值给局部变量elementData     
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        //每次获取完下一个元素后,游标向后移动一位    
        cursor = i + 1;
        //返回当前游标对应的元素
        return (E) elementData[lastRet = i];
    }
    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();
        }
    }
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}


并发修改异常ConcurrentModificationException详解(二)https://developer.aliyun.com/article/1393272

相关文章
|
1月前
|
存储 安全 Java
如何避免`ArrayStoreException`异常?
`ArrayStoreException`是在Java中尝试将错误类型的对象存储到泛型数组时抛出的异常。要避免此异常,需确保向数组添加的对象类型与数组声明的类型一致,使用泛型和类型检查,以及在运行时进行类型安全的转换和验证。
|
2月前
|
缓存 Linux C++
map异常崩溃分析汇总
文章讨论了std::map和std::set在某些情况下崩溃的原因,包括结构体字节对齐问题、多线程资源同步问题、以及比较器的实现问题,并提供了相应的解决方案。
|
3月前
|
安全 测试技术 数据库连接
如何避免 C# 中的异常
【8月更文挑战第27天】
50 2
|
3月前
|
Java
线程池中线程抛了异常,该如何处理?
【8月更文挑战第27天】在Java多线程编程中,线程池(ThreadPool)是一种常用的并发处理工具,它能够有效地管理线程的生命周期,提高资源利用率,并简化并发编程的复杂性。然而,当线程池中的线程在执行任务时抛出异常,如果不妥善处理,这些异常可能会导致程序出现未预料的行为,甚至崩溃。因此,了解并掌握线程池异常处理机制至关重要。
440 0
|
6月前
并发修改异常ConcurrentModificationException详解(二)
并发修改异常ConcurrentModificationException详解
87 0
并发修改异常ConcurrentModificationException详解(二)
|
存储 Go
当map在不提前分配内存的时候为什么会抛异常?
当map在不提前分配内存的时候为什么会抛异常?
如何处理 JDK 线程池内线程执行异常
如何处理 JDK 线程池内线程执行异常
136 2
你真的明白关于迭代器的方法、使用异常、并发修改异常介绍嘛?
关于迭代器的方法、使用异常、并发修改异常介绍的使用
137 0
你真的明白关于迭代器的方法、使用异常、并发修改异常介绍嘛?
|
存储 安全 Java
【HashMap并发修改异常】
【HashMap并发修改异常】
388 0