当前位置: 首页 > news >正文

ArrayList中的ConcurrentModificationException,并发修改异常,fail-fast机制。

一:什么时候出现?

  当我们用迭代器循环list的时候,在其中用list的方法新增/删除元素,就会出现这个错误。

package com.sinitek.aml;

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

public class ConcurrentModificationExceptionDemo {


    public static void main(String[] args) {


        List<String> arrayList = new ArrayList<>();

        arrayList.add("a");
        arrayList.add("b");
        arrayList.add("c");


        Iterator<String> iterator = arrayList.iterator();
        while(iterator.hasNext()){
            String tmp = iterator.next();
            if ("a".equals(tmp)){
                arrayList.remove(tmp);

            }
        }




    }
}

 

二:为什么会出这个错误? 

 如上图所示,运行代码就会有这个错误,我们看一下堆栈。

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.sinitek.aml.ConcurrentModificationExceptionDemo.main(ConcurrentModificationExceptionDemo.java:22)

  

  可以看到,是在ArrayList里面的Itr内部类里的checkForModification()方法里,抛出的这个异常,我们看下源码。

    final void checkForComodification() {
            //如果修改的次数 和 预期修改的次数不一致,就会报错
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }

 

  可以看到,上面两个关键的值是:modCount和expectModCount,其中的modCount来自ArrayList的父类,AbstractList,有兴趣的可以看下上面的翻译,也说明了这个字段的作用,正是为了fail-fast(快速失败)设计的。

  该值会在new出对象的时候,初始化为0;并且在每次的修改/删除操作时自增。

   

//arrayList的add方法,会先调用一次检查容量是否够的操作,我们可以看到,jdk专门在里面加了注释,会 Increments modCount, 增加modCount的值
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

    //确保新增元素容量大小够用的方法
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++; //首先先自增一次
    
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)   //判断最小需要的容量是否大于数组的长度(这里有个疑问,为什么一定相减然后判断是否>0,直接比较会有溢出的问题需要考虑么)
            grow(minCapacity);  //扩容
    }

    //删除方法,也会有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;
    }

  

那我们看一下,expectedModCount时怎么变化的。

  在ArrayList的内部类Itr(这个命名是不是有点随意了)里面,会看到expectedModCount是作为成员变量一起初始化的,所以我们在最开始的时候,调用list.iterator()方法的时候,就会初始化expectedModCount,将他变成和modCount一样的值。所以问题出在:

    1. new ArrayList,新增后,modCount会有一个初始值0。

    2. 调用iterator()方法后,会把expectedModCount的值初始化为modCount的值,此时,这两个值是一致的。

    3. 如果在循环里,使用list对象新增/删除对象(本文最上面所作的操作),modCount就会自增变化,而此时,expectedModCount却不会改变。

    4. 调用iterator里面的next() -> checkForComodification(),判断modCount==expectModCount的时候,就会发现不一致,从而抛出这个异常。

 

三:怎么解决。

  有两种方法。

    • 使用Itr类中的remove方法移除,但是如果用的是从Iterable继承的iterator返回的Iterator是没有add()方法的接口的,如果需要add(),那么需要调用list专有的迭代器listInterator()方法返回的ListIterator().
    • 不适用迭代器,使用原生的循环方式,这样就可以避免这个异常。(增强for只是一个语法糖(Syntactic Sugar)本质上还是使用了迭代器,也会有这个异常)。

 

四:为什么要定义这个异常?

    凡事多问一个为什么,jdk为什么要怎么设计,很简单,其实从命名上就能看出来,当我们在循环的时候,无论是我们在迭代循环的时候(目前我们就是这种情况),或者是其他的线程修改了这个(这个应该是jdk主要想解决的),都会出现数据不一致的情况,所以需要这个异常,让你fail-fast,早点异常早点再次尝试等。

  Q:为什么不在add()方法的时候,同时修改expectModCount,这样就他们一致了不就行了?

  A:add()方法在调用的时候,有可能没有这个Itr这个对象,而且单单修改值是没有用的,需要把对应的迭代器内部保存数据的值也对应的修改,只修改expectModCount有点类似掩耳盗铃。

  Q:那jdk可以新建一个重载方法,让itr对象当作参数,把他传进来,修改对应的保存数据的数组和对应的expectModCount,这样就能保证两个都一致了把。

  A:确实,这种方法对于上文这种fail-fast确实适用,但是还有一种情况就是:可能修改list的线程和迭代的线程都不是同一个,所以根本就没有itr这个对象,也就无法修改这个值了。

转载于:https://www.cnblogs.com/fenngz/p/10918923.html

相关文章:

  • vue-cli从2升级到3报错error 404 Not Found: @wry/context@^0.4.0
  • 创建数据结构库基础设施——异常类的构建
  • Windows下SVN的下载、安装
  • centOS7网络配置
  • angularJS 自定义服务
  • JqGrid纵向合并单元格
  • 线程池之ThreadPoolExecutor线程池源码分析笔记
  • Azure DevOps Server (TFS) 修改工作项附件大小限制
  • 二十三、NFS企业级优化
  • [百度百科]PCI-E的速度
  • AI numpy
  • zabbix 3.4 安装记录
  • 【原】常用shell命令
  • centos7下yum和python重装
  • 关于layui的日期和时间组件laydate闪屏的坑
  • @angular/forms 源码解析之双向绑定
  • Java的Interrupt与线程中断
  • Js基础知识(一) - 变量
  • PHP变量
  • Terraform入门 - 3. 变更基础设施
  • Travix是如何部署应用程序到Kubernetes上的
  • 前端面试之闭包
  • 如何学习JavaEE,项目又该如何做?
  • 什么软件可以剪辑音乐?
  • 赢得Docker挑战最佳实践
  • const的用法,特别是用在函数前面与后面的区别
  • 不要一棍子打翻所有黑盒模型,其实可以让它们发挥作用 ...
  • ​iOS实时查看App运行日志
  • # Swust 12th acm 邀请赛# [ E ] 01 String [题解]
  • (C#)一个最简单的链表类
  • (备忘)Java Map 遍历
  • (附源码)小程序 交通违法举报系统 毕业设计 242045
  • (十) 初识 Docker file
  • (转)http协议
  • (转)Linq学习笔记
  • (转)程序员疫苗:代码注入
  • ******IT公司面试题汇总+优秀技术博客汇总
  • .helper勒索病毒的最新威胁:如何恢复您的数据?
  • .htaccess配置常用技巧
  • .net framwork4.6操作MySQL报错Character set ‘utf8mb3‘ is not supported 解决方法
  • .NET 编写一个可以异步等待循环中任何一个部分的 Awaiter
  • .NET 反射的使用
  • .net知识和学习方法系列(二十一)CLR-枚举
  • ::什么意思
  • [ NOI 2001 ] 食物链
  • [20161101]rman备份与数据文件变化7.txt
  • [Android实例] 保持屏幕长亮的两种方法 [转]
  • [Android学习笔记]ScrollView的使用
  • [Assignment] C++1
  • [BUUCTF 2018]Online Tool
  • [C#]winform部署PaddleOCRV3推理模型
  • [C#C++]类CLASS
  • [C语言]——分支和循环(4)
  • [EFI]Lenovo ThinkPad X280电脑 Hackintosh 黑苹果引导文件
  • [flume$2]记录一个写自定义Flume拦截器遇到的错误