java.util.ConcurrentModificationException解决详解-程序员宅基地

技术标签: java  

异常产生

当我们迭代一个ArrayList或者HashMap时,如果尝试对集合做一些修改操作(例如删除元素),可能会抛出java.util.ConcurrentModificationException的异常。

package reyo.sdk.utils.test.list2;

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

public class AddRemoveList {

	public static void main(String args[]) {
		List<String> list = new ArrayList<String>();
		list.add("A");
		list.add("B");

		for (String s : list) {
			if (s.equals("B")) {
				list.remove(s);
			}
		}

		//foreach循环等效于迭代器
		/*Iterator<String> iterator=list.iterator();
		while(iterator.hasNext()){
		    String s=iterator.next();
		    if (s.equals("B")) {
		        list.remove(s);
		    }
		}*/
	}
}

 出错详情:

 

异常原因

ArrayList的父类AbstarctList中有一个域modCount,每次对集合进行修改(增添元素,删除元素……)时都会modCount++

而foreach的背后实现原理其实就是Iterator(关于Iterator可以看Java Design Pattern: Iterator),等同于注释部分代码。在这里,迭代ArrayList的Iterator中有一个变量expectedModCount,该变量会初始化和modCount相等,但如果接下来如果集合进行修改modCount改变,就会造成expectedModCount!=modCount,此时就会抛出java.util.ConcurrentModificationException异常

过程如下图:

我们再来根据源码详细的走一遍这个过程

/*
 *AbstarctList的内部类,用于迭代
 */
private class Itr implements Iterator<E> {
    int cursor = 0;   //将要访问的元素的索引
    int lastRet = -1;  //上一个访问元素的索引
    int expectedModCount = modCount;//expectedModCount为预期修改值,初始化等于modCount(AbstractList类中的一个成员变量)

    //判断是否还有下一个元素
    public boolean hasNext() {
            return cursor != size();
    }
    //取出下一个元素
    public E next() {
            checkForComodification();  //关键的一行代码,判断expectedModCount和modCount是否相等
        try {
        E next = get(cursor);
        lastRet = cursor++;
        return next;
        } catch (IndexOutOfBoundsException e) {
        checkForComodification();
        throw new NoSuchElementException();
        }
    }

    public void remove() {
        if (lastRet == -1)
        throw new IllegalStateException();
            checkForComodification();

        try {
        AbstractList.this.remove(lastRet);
        if (lastRet < cursor)
            cursor--;
        lastRet = -1;
        expectedModCount = modCount;
        } catch (IndexOutOfBoundsException e) {
        throw new ConcurrentModificationException();
        }
    }

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

 

/* *AbstarctList的内部类,用于迭代 */ private class Itr implements Iterator<E> { int cursor = 0; //将要访问的元素的索引 int lastRet = -1; //上一个访问元素的索引 int expectedModCount = modCount;//expectedModCount为预期修改值,初始化等于modCount(AbstractList类中的一个成员变量) //判断是否还有下一个元素 public boolean hasNext() { return cursor != size(); } //取出下一个元素 public E next() { checkForComodification(); //关键的一行代码,判断expectedModCount和modCount是否相等 try { E next = get(cursor); lastRet = cursor++; return next; } catch (IndexOutOfBoundsException e) { checkForComodification(); throw new NoSuchElementException(); } } public void remove() { if (lastRet == -1) throw new IllegalStateException(); checkForComodification(); try { AbstractList.this.remove(lastRet); if (lastRet < cursor) cursor--; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException e) { throw new ConcurrentModificationException(); } } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }


作者:MrDTree
链接:http://www.jianshu.com/p/c5b52927a61a
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
根据代码可知,每次迭代list时,会初始化Itr的三个成员变量
int cursor = 0;   //将要访问的元素的索引
int lastRet = -1;  //上一个访问元素的索引
int expectedModCount = modCount; //预期修改值,初始化等于modCount(AbstractList类中的一个成员变量)

 

接着调用hasNext()循环判断访问元素的下标是否到达末尾。如果没有,调用next()方法,取出元素。
而最上面测试代码出现异常的原因在于,next()方法调用checkForComodification()时,发现了modCount != expectedModCount

接下来我们看下ArrayList的源码,了解下modCount 是如何与expectedModCount不相等的。

public boolean add(E paramE) {  
    ensureCapacityInternal(this.size + 1);  
    /** 省略此处代码 */  
}  

private void ensureCapacityInternal(int paramInt) {  
    if (this.elementData == EMPTY_ELEMENTDATA)  
        paramInt = Math.max(10, paramInt);  
    ensureExplicitCapacity(paramInt);  
}  

private void ensureExplicitCapacity(int paramInt) {  
    this.modCount += 1;    //修改modCount  
    /** 省略此处代码 */  
}  

public boolean remove(Object paramObject) {  
    int i;  
    if (paramObject == null)  
        for (i = 0; i < this.size; ++i) {  
            if (this.elementData[i] != null)  
                continue;  
            fastRemove(i);  
            return true;  
        }  
    else  
        for (i = 0; i < this.size; ++i) {  
            if (!(paramObject.equals(this.elementData[i])))  
                continue;  
            fastRemove(i);  
            return true;  
        }  
    return false;  
}  

private void fastRemove(int paramInt) {  
    this.modCount += 1;   //修改modCount  
    /** 省略此处代码 */  
}  

public void clear() {  
    this.modCount += 1;    //修改modCount  
    /** 省略此处代码 */  
}

 

从上面的代码可以看出,ArrayList的add、remove、clear方法都会造成modCount的改变。迭代过程中如何调用这些方法就会造成modCount的增加,使迭代类中expectedModCount和modCount不相等。

异常的解决

1. 单线程环境

好,现在我们已经基本了解了异常的发送原因了。接下来我们来解决它。
我很任性,我就是想在迭代集合时删除集合的元素,怎么办?

Iterator<String> iter = list.iterator();
while(iter.hasNext()){
    String str = iter.next();
      if( str.equals("B") )
      {
        iter.remove();
      }
}

 

细心的朋友会发现Itr中的也有一个remove方法,实质也是调用了ArrayList中的remove,但增加了expectedModCount = modCount;保证了不会抛出java.util.ConcurrentModificationException异常。

但是,这个办法的有两个弊端
1.只能进行remove操作,add、clear等Itr中没有。
2.而且只适用单线程环境。

2. 多线程环境

在多线程环境下,我们再次试验下上面的代码

public class Test2 {
    static List<String> list = new ArrayList<String>();

    public static void main(String[] args) {
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");

        new Thread() {
            public void run() {
                Iterator<String> iterator = list.iterator();

                while (iterator.hasNext()) {
                    System.out.println(Thread.currentThread().getName() + ":"
                            + iterator.next());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
        }.start();

        new Thread() {
            public synchronized void run() {
                Iterator<String> iterator = list.iterator();

                while (iterator.hasNext()) {
                    String element = iterator.next();
                    System.out.println(Thread.currentThread().getName() + ":"
                            + element);
                    if (element.equals("c")) {
                        iterator.remove();
                    }
                }
            };
        }.start();

    }
}

 

异常的原因很简单,一个线程修改了list的modCount导致另外一个线程迭代时modCount与该迭代器的expectedModCount不相等。

此时有两个办法:

1,迭代前加锁,解决了多线程问题,但还是不能进行迭代add、clear等操作

public class Test2 {
 static List<String> list = new ArrayList<String>();

 public static void main(String[] args) {
     list.add("a");
     list.add("b");
     list.add("c");
     list.add("d");

     new Thread() {
         public void run() {
             Iterator<String> iterator = list.iterator();

             synchronized (list) {
                 while (iterator.hasNext()) {
                     System.out.println(Thread.currentThread().getName()
                             + ":" + iterator.next());
                     try {
                         Thread.sleep(1000);
                     } catch (InterruptedException e) {
                         // TODO Auto-generated catch block
                         e.printStackTrace();
                     }
                 }
             }
         };
     }.start();

     new Thread() {
         public synchronized void run() {
             Iterator<String> iterator = list.iterator();

             synchronized (list) {
                 while (iterator.hasNext()) {
                     String element = iterator.next();
                     System.out.println(Thread.currentThread().getName()
                             + ":" + element);
                     if (element.equals("c")) {
                         iterator.remove();
                     }
                 }
             }
         };
     }.start();

 }
}

 2,采用CopyOnWriteArrayList,解决了多线程问题,同时可以add、clear等操作

public class Test2 {
 static List<String> list = new CopyOnWriteArrayList<String>();

 public static void main(String[] args) {
     list.add("a");
     list.add("b");
     list.add("c");
     list.add("d");

     new Thread() {
         public void run() {
             Iterator<String> iterator = list.iterator();

                 while (iterator.hasNext()) {
                     System.out.println(Thread.currentThread().getName()
                             + ":" + iterator.next());
                     try {
                         Thread.sleep(1000);
                     } catch (InterruptedException e) {
                         // TODO Auto-generated catch block
                         e.printStackTrace();
                     }
                 }
         };
     }.start();

     new Thread() {
         public synchronized void run() {
             Iterator<String> iterator = list.iterator();

                 while (iterator.hasNext()) {
                     String element = iterator.next();
                     System.out.println(Thread.currentThread().getName()
                             + ":" + element);
                     if (element.equals("c")) {
                         list.remove(element);
                     }
                 }
         };
     }.start();

 }
}

 CopyOnWriteArrayList也是一个线程安全的ArrayList,其实现原理在于,每次add,remove等所有的操作都是重新创建一个新的数组,再把引用指向新的数组。

深入理解异常—fail-fast机制

到这里,我们似乎已经理解完这个异常的产生缘由了。
但是,仔细思考,还是会有几点疑惑:

  1. 既然modCount与expectedModCount不同会产生异常,那为什么还设置这个变量
  2. ConcurrentModificationException可以翻译成“并发修改异常”,那这个异常是否与多线程有关呢?

我们来看看源码中modCount的注解

/**
     * The number of times this list has been <i>structurally modified</i>.
     * Structural modifications are those that change the size of the
     * list, or otherwise perturb it in such a fashion that iterations in
     * progress may yield incorrect results.
     *
     * <p>This field is used by the iterator and list iterator implementation
     * returned by the {@code iterator} and {@code listIterator} methods.
     * If the value of this field changes unexpectedly, the iterator (or list
     * iterator) will throw a {@code ConcurrentModificationException} in
     * response to the {@code next}, {@code remove}, {@code previous},
     * {@code set} or {@code add} operations.  This provides
     * <i>fail-fast</i> behavior, rather than non-deterministic behavior in
     * the face of concurrent modification during iteration.
     *
     * <p><b>Use of this field by subclasses is optional.</b> If a subclass
     * wishes to provide fail-fast iterators (and list iterators), then it
     * merely has to increment this field in its {@code add(int, E)} and
     * {@code remove(int)} methods (and any other methods that it overrides
     * that result in structural modifications to the list).  A single call to
     * {@code add(int, E)} or {@code remove(int)} must add no more than
     * one to this field, or the iterators (and list iterators) will throw
     * bogus {@code ConcurrentModificationExceptions}.  If an implementation
     * does not wish to provide fail-fast iterators, this field may be
     * ignored.
     */
    protected transient int modCount = 0;

 我们注意到,注解中频繁的出现了fail-fast
那么fail-fast(快速失败)机制是什么呢?

“快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。

 

看到这里,我们明白了,fail-fast机制就是为了防止多线程修改集合造成并发问题的机制嘛。
之所以有modCount这个成员变量,就是为了辨别多线程修改集合时出现的错误。而java.util.ConcurrentModificationException就是并发异常。
但是单线程使用不单时也可能抛出这个异常。

 

转载于:https://www.cnblogs.com/interdrp/p/7729995.html

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/adknuf1202/article/details/102167792

智能推荐

mysql5.6.47编译安装错误记录_ncurses-compat-libs-程序员宅基地

文章浏览阅读1.8k次。1.编译安装完mysql5.6.47后报错[ root@ct8 init.d]#mysqlmysql: error while loading shared libraries: libncurses.so.5: cannot open shared object file: No such file or directory2.翻译后提示缺少依赖,没有找到问价后目录3.根据提示搜索依赖来自于哪个包[ root@ct8 init.d]#yum provides libncurses.._ncurses-compat-libs

实用开发平台对比分析-程序员宅基地

文章浏览阅读424次。2019独角兽企业重金招聘Python工程师标准>>> ..._云捷配源码

SPSS(十八)SPSS之信度分析(图文+数据集)_spss中信度分析-程序员宅基地

文章浏览阅读4.1w次,点赞19次,收藏237次。SPSS(十八)SPSS之信度分析(图文+数据集)首先我么先了解信度是什么,能做什么:信度分析简介探索研究事物间的相似性 真分数测量理论的基本假设实际得分与真分数存在线性关系:X = T + E测量误差的期望为零:E(e)=0误差与真分数彼此独立:实际分数的方差=真分数的方差+随机误差的方差:在真分数测量理论中,信度的定义是真分数方差与实际分数方差的比值,..._spss中信度分析

Oracle数据库整体简介_小葳宝贝最爱吃饭的博客-程序员宅基地

文章浏览阅读294次。Oracle数据库整体简介数据库基础概念Oracle数据库Oracle数据库逻辑结构:Oracle物理结构Oracle实例数据库基础概念管理数据的发展历程:人工管理数据、文件系统管理数据、数据库系统管理数据。数据库管理数据的模型:层次模型、网状模型、关系模型。数据库概念:存放数据的仓库。数据库系统:采用数据库技术的计算机系统,包括数据库和数据库管理系统。数据库系统由下列几部分组成:计算机硬件:提供基本的操作环境。数据库(DB):保存各种各样的数据。数据库管理系统(DBMS):提供管理数据

yum配置源问题记录--[Errno 14] FTP Error 550 - Server denied you to change to the given directoryA-程序员宅基地

文章浏览阅读1.1w次,点赞7次,收藏28次。最近集团私有云上了一个项目,工作分配给了新来的小伙子,让他重做部分服务器系统后搭建私有的Yum仓库。仓库是搭建好了,但是他反馈给我,无法使用,一直报错:[Errno 14] FTP Error 550 - Server denied you to change to the given directoryA。这里进行记录下排查及解决思路。1.现象描述(1)安装完毕Yum仓库后,镜像仓库主机安装..._[errno 14]

Python: 计算百分比_python计算百分比_laoyouzhazi的博客-程序员宅基地

文章浏览阅读1.2w次。Python: 计算百分比题目说明方法一:浮点数有效值代码示例一示例二示例三示例四方法二:取整取余代码示例一示例二示例三笔记题目说明假设变量a,b,其中b不等于0。限定小数后有效位数为2,求a对于b的百分比。方法一:浮点数有效值代码def format_percentage(a, b): p = 100 * a / b if p == 0.0: q = '0%' else: q = '%.2f%%' % p return q示例一print(format_percentage_python计算百分比

随便推点

数据库——彻底明白超键、候选键、主键、外键-程序员宅基地

文章浏览阅读4.6w次,点赞165次,收藏327次。知识就是一遍又一遍的学,每次的学习都有不一样的收获和感受,然后得到了收获就该是分享的过程了,很多人都不易区分超键、候选键、主键、外键这四个键的区别,下来为了大家少走弯路,特总结了此篇博客!1、书中的定义超键(super key): 在关系中能唯一标识元组的属性集称为关系模式的超键候选键(candidate key): 不含有多余属性的超键称为候选键。也就是在候选键中,若再删除属性,就不是键了!主键(_超键

org.hibernate.hql.internal.ast.QuerySyntaxException-程序员宅基地

文章浏览阅读572次。org.hibernate.hql.internal.ast.QuerySyntaxException: customer is not mapped [from customer where name like ?] at org.hibernate.hql.internal.ast.util.SessionFactoryHelper.requireClassPersister(Sessi

word交叉引用——在正文中给参考文献做链接_word 引用链接到引用-程序员宅基地

文章浏览阅读1.1w次,点赞4次,收藏20次。word交叉引用只需两步即可,3分钟即会第一步: 插入编号文档中有如下参考文献,但还没有[1],[2],[3]等编号。那么先给参考文献编号序号,使用word—>开始—>编号接下来给参考文献标好序号,光标选到某参考文献前,点编号即可自动编号,如下图所示标完序号如图所示第二步:正文引用编号将光标定位在正文中需要插入参考文献上标的地方,点击“..._word 引用链接到引用

Web开发基础(一)-程序员宅基地

文章浏览阅读309次。由于最近开发公司的后台管理系统和服务器的需求,所以临时学点WEB开发基础,因为是内部人员使用的,要求不是很高。所以也没有学得很精通,就随便学习一下基础就开始上手做。一、HTML1、图片标签&lt;img src="链接" width="150px" height="30px" alt="logo图片"/&gt;2、超链接标签&lt;a href="链接地址&q

python 的继承 直接调用父类方法及super-程序员宅基地

文章浏览阅读112次。class Person(object): def __init__(self,name): self.name = name def getname(self): print self.nameclass Student(Person): def __init__(self,name,age): #P...

QGC地面站PC桥接px4(QGC+wifi+机载计算机+px4)_qgc连接 串口 无线_硬梆梆的马里奥的博客-程序员宅基地

文章浏览阅读3k次,点赞6次,收藏40次。QGC地面站PC桥接px4(QGC+wifi+机载计算机+px4)1 在机载计算机上安装ubuntu2 安装ros3 机载计算机上安装mavros1安装mavros2 安装安装mavros相关的 geographiclib dataset4 安装ssh服务5 连接px4和机载计算机6 地面站PC用ssh控制机载计算机7 QGC上建立通讯连接用px4做的无人机,由于手头没有数传,需要进行无人机姿态环和位置环调参,通过QGC地面站PC桥接px4实现读取飞控状态参数的功能。1 在机载计算机上安装ubuntu_qgc连接 串口 无线

推荐文章

热门文章

相关标签