目录

一、简介

二、异常原因分析

三、异常原因追踪

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

五、总结

一、简介

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

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

下面看一个示例:

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

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

Exception in thread "main" java.util.ConcurrentModificationExceptionat 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<E> {//游标, 每获取一次元素,游标会向后移动一位int cursor;       // index of next element to returnint 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为10minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);}ensureExplicitCapacity(minCapacity);
}private void ensureExplicitCapacity(int minCapacity) {//集合结构修改次数加一modCount++;// overflow-conscious codeif (minCapacity - elementData.length > 0)//扩容方法,扩容后是原容量的1.5倍//扩容前:数组长度10  扩容后:数组长度变为10 + (10 / 2) = 15grow(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<E> iterator() {return new Itr();
}

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

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

private class Itr implements Iterator<E> {//cursor初始值为0,每次取出一个元素,cursor值会+1,以便下一次能指向下一个元素,直到cursor值等于集合的长度为止int cursor;       // index of next element to returnint lastRet = -1; // index of last element returned; -1 if no such//初始化预期修改次数为实际修改次数modCount,即上图中的3int expectedModCount = modCount;//判断是否还有下一个元素:通过比较游标cursor是否等于数组的长度//因为集合中最后一个元素的索引为size-1,只要cursor值不等于size,证明还有下一个元素,此时hasNext方法返回true,//如果cursor值与size相等,那么证明已经迭代到最后一个元素,返回falsepublic 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();}
}

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

第一次调用hasNext(): cursor = 0    size = 3
第一次调用iterator.next():第一次调用checkForComodification():modCount = 3  expectedModCount = 3
由于 modCount = expectedModCount ,不会发生并发修改异常。并且返回当前游标对应的值,即返回1.
由于满足Integer.parseInt(nextElement) < 2,所以会执行list.add("2")方法,之前已经了解到,add()方法会
修改modCount的值 + 1· 所以此时modCount的值变为4了.
第一次next()方法调用完,cursor游标的值会加一,所以cursor = 1. ===============================================================================================================
第二次调用hasNext(): cursor = 1  size = 4
第二次调用iterator.next():第二次调用checkForComodification():modCount = 4  expectedModCount = 3
由于 modCount != expectedModCount ,此时会发生并发修改异常。以上就是ConcurrentModificationException一场产生的简单解析过程。  

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

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

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

public class TestConcurrentModifyException {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("chinese");list.add("math");list.add("english");Iterator<String> iterator = list.iterator();while (iterator.hasNext()) {String nextElement = iterator.next();if ("english".equals(nextElement)) {//使用ArrayList的boolean remove(Object o)方法进行删除list.remove("english");}}}
}

程序运行结果:

Exception in thread "main" java.util.ConcurrentModificationExceptionat 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:19)

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

remove(Object o)方法源码:

public boolean remove(Object o) {//判断需要删除的元素是否为nullif (o == null) {for (int index = 0; index < size; index++)if (elementData[index] == null) {fastRemove(index);return true;}} else {//不为null,遍历集合,使用equals进行比较是否相等for (int index = 0; index < size; index++)if (o.equals(elementData[index])) {fastRemove(index);return true;}}return false;
}private void fastRemove(int index) {//删除元素时,实际修改次数会自增1//此时: modCount实际修改次数为4,但是预期修改次数还是获取迭代器时候的3,两者已经不一致了。modCount++;//计算集合需要移动元素的个数int numMoved = size - index - 1;if (numMoved > 0)//数组拷贝System.arraycopy(elementData, index+1, elementData, index,numMoved);//将删除元素置为null,利于垃圾回收elementData[--size] = null; // clear to let GC do its work
}

我们分析一下程序的执行过程,查看并发修改异常是怎么产生的。当我们执行到下面一行语句之后,集合的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",如果存在则删除。

public class TestConcurrentModifyException {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("chinese");list.add("math");list.add("english");Iterator<String> iterator = list.iterator();while (iterator.hasNext()) {String nextElement = iterator.next();if ("math".equals(nextElement)) {//使用ArrayList的boolean remove(Object o)方法进行删除list.remove("math");}}System.out.println(list);}
}

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

程序运行结果:

[chinese, english]

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

第一次调用hasNext(): cursor = 0    size = 3, hasNext()返回true
第一次调用iterator.next():第一次调用checkForComodification():modCount = 3  expectedModCount = 3
由于 modCount = expectedModCount ,不会发生并发修改异常。
第一次next()方法调用完,cursor游标的值会加一,所以cursor = 1. ===============================================================================================================
第二次调用hasNext(): cursor = 1  size = 3, hasNext()返回true
第二次调用iterator.next():第二次调用checkForComodification():modCount = 3  expectedModCount = 3
由于 modCount = expectedModCount ,不会发生并发修改异常。
第二次next()方法调用完,cursor游标的值会加一,所以cursor = 2.
由于上面的示例中,"math"元素刚好在第二个,所以这时候"math".equals(nextElement)会返回true,
所以会执行集合的删除元素方法,size会减一,实际修改次数modCount会加一,所以size = 2  modCount = 4ps:这里注意cursor游标的值也是2,size的值也是2,===============================================================================================================
第三次调用hasNext(): cursor = 2  size = 2, 两者相等,所以hasNext()返回false,while循环结束,意味着不会调用next()方法,
不会执行调用checkForComodification()方法,那么肯定就不会发生并发修改异常。

小结论:

  • 当要删除的元素在集合中的倒数第二个元素的时候,删除元素不会产生并发修改异常。
  • 原因:因为在调用hasNext()方法的时候,cursor = size是相等的,hasNext()方法会返回false, 所以不会执行next()方法,也就不会调用checkForComodification()方法,就不会发生并发修改异常。

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

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

//迭代器自带的删除方法
public void remove() {if (lastRet < 0)throw new IllegalStateException();//校验是否产生并发修改异常    checkForComodification();try {//真正删除元素的方法还是调用的ArrayList的删除方法//根据索引进行删除ArrayList.this.remove(lastRet);cursor = lastRet;lastRet = -1;//每次删除完成后,会重新将expectedModCount重新赋值,值就是实际修改次数modCount的值//这就保证了,实际修改次数modCount一定会等于预期修改次数expectedModCount ,所以不会产生并发修改异常.expectedModCount = modCount;} catch (IndexOutOfBoundsException ex) {throw new ConcurrentModificationException();}
}

小结论:

  • 迭代器调用remove()方法删除元素,底层还是调用的集合的删除元素的方法;
  • 在调用remove()方法后,都会将modCount的值赋值给expectedModCount,保证了它两的值永远都是相等的,所以也就不会产生并发修改异常;

五、总结

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

并发修改异常ConcurrentModificationException详解相关推荐

  1. 理解和解决Java并发修改异常ConcurrentModificationException(转载)

    原文地址:https://www.jianshu.com/p/f3f6b12330c1 理解和解决Java并发修改异常ConcurrentModificationException 不知读者在Java ...

  2. java 并发修改_理解和解决Java并发修改异常ConcurrentModificationException

    your name.jpg 关键字: Java Exception 不知读者在Java开发的过程中有没有遇到类似的异常信息 Exception in thread "main" j ...

  3. php 迭代器迭代中文时重复,3种方式解决iterator迭代器并发修改异常

    3种方式解决iterator迭代器并发修改异常ConcurrentModificationException 在使用迭代器的时候,时长会遇到 ConcurrentModificationExcepti ...

  4. Java并发--ConcurrentModificationException(并发修改异常)异常原因和解决方法

    Java并发--ConcurrentModificationException(并发修改异常)异常原因和解决方法 参考文章: (1)Java并发--ConcurrentModificationExce ...

  5. java.util.ConcurrentModificationException 异常问题详解

    java.util.ConcurrentModificationException 异常问题详解 参考文章: (1)java.util.ConcurrentModificationException ...

  6. ConcurrentModificationException 并发修改异常剖析及解决方案

    最近在回头打基础学习Java SE,研究List集合的过程中,遇到了ConcurrentModificationException 并发修改异常.在此记录下遇到问题的原因解析和解决方案.错误千奇百怪, ...

  7. C++11 并发指南四(future 详解二 std::packaged_task 介绍)

    上一讲<C++11 并发指南四(<future> 详解一 std::promise 介绍)>主要介绍了 <future> 头文件中的 std::promise 类, ...

  8. C++11 并发指南三(Lock 详解)(转载)

    multithreading 多线程 C++11 C++11多线程基本使用 C++11 并发指南三(Lock 详解) 在 <C++11 并发指南三(std::mutex 详解)>一文中我们 ...

  9. Java并发编程最佳实例详解系列

    Java并发编程最佳实例详解系列: Java并发编程(一)线程定义.状态和属性 Java并发编程(一)线程定义.状态和属性 线程是指程序在执行过程中,能够执行程序代码的一个执行单元.在java语言中, ...

  10. 第三次学JAVA再学不好就吃翔(part79)--并发修改异常产生的原因及解决方案

    学习笔记,仅供参考,有错必纠 文章目录 集合 并发修改异常产生的原因及解决方案 ConcurrentModificationException 解决方案 集合 并发修改异常产生的原因及解决方案 在这个 ...

最新文章

  1. Redis RDB 持久化详解
  2. 南海有macbook吗?
  3. VS2005 ATL WINDOWS服务感想
  4. Asp.Net Core 单元测试正确姿势
  5. VB.NET 中的 As New 以及型別指定
  6. Linux下安装ActiveMQ(CentOS7)
  7. 英特尔cpu发布时间表_英特尔10代桌面cpu上市时间(英特尔10代发售时间)
  8. Android手机玩8086汇编
  9. 《心流》| 成年人的友谊,甜蜜与苦涩交杂
  10. 项目管理第三招:做好计划,拥抱变化
  11. _3_body_标签
  12. C语言goto语句 做一个自动关机小程序 —— 给小伙伴来个“恶作剧”吧
  13. 使用人人开源项目遇到的一些坑
  14. android圆形菜单,android 圆形旋转菜单例子
  15. HDU 2094 产生冠军
  16. 如何成为一名合格的运筹优化算法工程师?
  17. Pycharm以及cmd调用Anaconda已配置环境的方法
  18. 2015-4-20分享的pdf
  19. 中国联通大幅下调国际漫游费
  20. (附源码)springboot网络微小说 毕业设计031758

热门文章

  1. button html ios,iOS实现UIButton图标和文字上下布局
  2. java list 模拟查询_java 模拟简单搜索
  3. Mac OS 区块链hyperledger环境搭建、环境架构介绍、环境如何用、部署 Chaincode、智能合约的调用
  4. 算法: 用队列Queue实现栈Stack
  5. 基于python的电商评论分析_Python实现爬取并分析电商评论
  6. Torch7 out of memory 解决方法
  7. UnboundLocalError: local variable 'XXX' referenced before assignment
  8. python备注(持续更新……)
  9. 【14年浙江省赛 ZOJ 3778】Talented Chef
  10. 【状压dp】【最短Hamilton路径】