并发修改异常ConcurrentModificationException详解

智能总结

本文详细讲解了多线程编程中的并发修改异常ConcurrentModificationException。介绍了异常产生原因,如ArrayList中modCount和expectedModCount不相等时触发。通过示例Debug调试追踪异常产生过程,分析特殊情况。还阐述了避免异常的方法,即使用Iterator迭代器的remove方法,因其能保证modCount和expectedModCount值相等。最后总结强调应使用迭代器提供的删除方法避免异常。

一、简介

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

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

下面看一个示例:

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

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

二、异常原因分析

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

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【预期修改次数】。

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

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

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

  • remove():每删除一个元素,modCount的值会自增一次

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

三、异常原因追踪

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

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

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

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

继续Debug,我们记录一下几次hasNext()/next()方法时,其中几个重要变量值的变化过程。

下图是发生并发修改异常时checkForComodification()方法的执行过程,注意modCount和expectedModCount 的值:

四、并发修改异常的特殊情况

示例:已知集合中有三个元素:"chinese"、"math"、"english",使用迭代器进行遍历, 判断集合中存在"english",如果存在则删除。

程序运行结果:

通过上面的分析,由于往集合中加入了三个元素,所以modCount实际修改次数的值为3,当我们调用iterator()获取迭代器的时候,初始化expectedModCount的值也为3。下面我们一起看一下ArrayList类中的根据元素删除方法的源码。

remove(Object o)方法源码:

我们分析一下程序的执行过程,查看并发修改异常是怎么产生的。当我们执行到下面一行语句之后,集合的size会减1,所以此时size = 2.

list.remove("english");

那么这时候再次执行下面的判断

while (iterator.hasNext()) {

此时cursor的值是3,但是size的值是2,两者不相等,所以hasNext()方法返回true,意味着集合中还有元素,所以还会执行一次next()方法,此时执行checkForComodification()方法,判断modCount是否等于expectedModCount,(expectedModCount=3, modCount=4),两者不相等,所以这就抛出了并发修改异常。

小结论:

  1. 集合每次调用add方法时,实际修改次数的值modCount都会自增1;

  2. 在获取迭代器的时候,集合只会执行一次将实际修改集合的次数modCount的值赋值给预期修改的次数变量expectedModCount;

  3. 集合在删除元素的时候,也会针对实际修改次数modCount的变量进行自增操作;

下面再来看一个并发修改异常的特殊情况,观察下面的程序:

示例:已知集合中有三个元素:"chinese"、"math"、"english",使用迭代器进行遍历,判断集合中存在"math",如果存在则删除。

我们可以看到,这个示例跟上面一个实例非常相似,唯一不同的是这次删除的元素是集合中的倒数第二个元素。

程序运行结果:

[chinese, english]

我们看到,这里并没有发生并发修改异常,很神奇,而且成功删除”math“这个元素,这是为什么呢?上面一个示例明明说了会发生并发修改异常。下面我们还是分析一下其中的特殊原因:

小结论:

  1. 当要删除的元素在集合中的倒数第二个元素的时候,删除元素不会产生并发修改异常。

  2. 原因:因为在调用hasNext()方法的时候,cursor = size是相等的,hasNext()方法会返回false, 所以不会执行next()方法,也就不会调用checkForComodification()方法,就不会发生并发修改异常。

四、如何避免并发修改异常?

如何避免并发修改异常还有它的特殊情况呢,其实Iterator迭代器里面已经提供了remove(),用于在迭代过程对集合结构进行修改,使用iterator.remove()不会产生并发修改异常,为什么迭代器的删除方法不会产生异常呢,我们得去看看Itr内部类的remove()源码:

小结论:

  1. 迭代器调用remove()方法删除元素,底层还是调用的集合的删除元素的方法;

  2. 在调用remove()方法后,都会将modCount的值赋值给expectedModCount,保证了它两的值永远都是相等的,所以也就不会产生并发修改异常;

五、总结

以上通过几个示例讲解了并发修改异常的现象,以及分析了并发修改异常是如何产生的,在实际工作中,如果需要使用到删除集合中元素,那么我们不要使用集合自带的删除方法,我们应该使用iterator迭代器给我们提供的删除方法,这样可以很大程序避免程序发生并发修改异常ConcurrentModificationException。


作者:终有救赎
链接:https://juejin.cn/post/7289662478787051579

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ok060

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值