java并发编程之ConcurrentModificationException详解

java并发编程之ConcurrentModificationException详解

 

在对容器进行迭代的情况下,我们可能遇到过ConcurrentModificationException这个异常,这是因为在设计迭代器时没有考虑到并发修改的问题,所以引用了ConcurrentModificationException这个善意的异常来警示开发者,这种策略叫做“及时失败”-fail-fast注意ConcurrentModificationException不仅仅只是在多线程操作的情况下会出现,在单线程的情况下也可能会出现。先模拟一个单线程的情况下出现该异常的情况,并且从源码的角度分析异常产生的原因,最后如何避免出现该异常

1,单线程出现ConcurrentModificationException

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

public class ConcurrentModificationExceptionTest {
	private List<Integer> list = new ArrayList<Integer>();
	public static void main(String[] args) {
		ConcurrentModificationExceptionTest cme = new ConcurrentModificationExceptionTest();
		//首先往list中增加内容
		for(int i=0;i<20;i++) {
			cme.list.add(i);//自动装箱
		}
		//迭代操作
		Iterator<Integer> ite = cme.list.iterator();
		while(ite.hasNext()) {
			Integer integer = ite.next();
			if(integer == 10) {//自动拆箱
				cme.list.remove(integer);
			}
		}
	}
}

运行上述代码出现异常

java并发编程之ConcurrentModificationException详解

 

2,异常分析

从报错的位置可以看出报错的代码为Integer integer = ite.next(),那么我们来看看Iterator中的next()方法,注意该arraylist中得iterator是实现了Iterator接口的内部类

public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

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

发生报错的位置是checkForComodification(),这个方法里面比较modcount(修改次数)和expectedmodCount(期望修改次数),也就是说报错的原因是这两个值不相等。好,那我们来跟踪这两个值。expectedModCount是iterator内部类的属性,在初始化的时候int expectedModCount = modCount,后面没有再进行更改过。protected transient int modCount = 0;modcount修改的地方比较多,在add、remove、clear、ensureCapacityInternal等,凡是设计到ArrayList对象修改的都会自增modCount属性。简而言之,就是每次修改arraylist对象都会引起modCount自增,所以就导致在代码cme.list.remove(integer);运行后modCount又自增了一次,导致expectedmodCount!=modCount,导致出现了ConcurrentModificationException

3,如何避免

使用迭代器进行删除iterator.remove()替代list.remove();

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

public class ConcurrentModificationExceptionTest {
	private List<Integer> list = new ArrayList<Integer>();
	public static void main(String[] args) {
		ConcurrentModificationExceptionTest cme = new ConcurrentModificationExceptionTest();
		//首先往list中增加内容
		for(int i=0;i<20;i++) {
			cme.list.add(i);//自动装箱
		}
		//迭代操作
		Iterator<Integer> ite = cme.list.iterator();
		while(ite.hasNext()) {
			Integer integer = ite.next();
			if(integer == 10) {//自动拆箱
				//cme.list.remove(integer);
				ite.remove();
			}
		}
	}
}

我们看下为什么使用iterator.remove()就不会抛异常呢,直接看源码

public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);//最终也是掉了list中的删除
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;//这是重点,保证了两者相等
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

从上述可以看到iterator的remove方法不仅删除了arraylist容器中的对象,它还加了一句expectedModCount = modCount;这样就保证再调用next()方法时就不会抛异常了。

 

单线程讨论完了,我们来看看多线程

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

public class ConcurrentModificationExceptionTest {
	private List<Integer> list = new ArrayList<Integer>();
	public static void main(String[] args) {
		ConcurrentModificationExceptionTest cme = new ConcurrentModificationExceptionTest();
		//首先往list中增加内容
		for(int i=0;i<20;i++) {
			cme.list.add(i);//自动装箱
		}
		//迭代操作
		/*Iterator<Integer> ite = cme.list.iterator();
		while(ite.hasNext()) {
			Integer integer = ite.next();
			if(integer == 10) {//自动拆箱
				//cme.list.remove(integer);
				ite.remove();
			}
		}*/
		Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                Iterator<Integer> iterator = cme.list.iterator();
                while (iterator.hasNext()) {
                    System.out.println("thread1 " + iterator.next());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
            	Iterator<Integer> iterator = cme.list.iterator();
                while (iterator.hasNext()) {
                    System.out.println("thread2 " + iterator.next());
                    iterator.remove();
                }
            }
        });
        thread1.start();
        thread2.start();
	}
}

运行结果:

java并发编程之ConcurrentModificationException详解

 

分析:

上述代码中thead1时进行了迭代操作,thead2进行了迭代和删除操作,从打印的结果来看,当thead1遍历第一个内容之后进行了sleep操作,1s中后,继续进行遍历,这个时候由于thead2进行了remove操作,则修改了arraylist中的modcount的值,虽然也修改了expectedmodcount,但是thead2修改的expecedmodcount时thread2创建的iterator中的expectedmodcount,和thead1中的expectedmodcount没关系,此时expectedmodcount还是20,而arraylist是thead1和thead2共享的变量,thead2修改了arraylist中的modcount导致了thead1迭代的时候expectedModCount!=modcount,因此抛异常了。

解决:使用同步的方法,将并行的操作变成串行操作

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

public class ConcurrentModificationExceptionTest {
	private List<Integer> list = new ArrayList<Integer>();
	public static void main(String[] args) {
		ConcurrentModificationExceptionTest cme = new ConcurrentModificationExceptionTest();
		//首先往list中增加内容
		for(int i=0;i<20;i++) {
			cme.list.add(i);//自动装箱
		}
		//迭代操作
		/*Iterator<Integer> ite = cme.list.iterator();
		while(ite.hasNext()) {
			Integer integer = ite.next();
			if(integer == 10) {//自动拆箱
				//cme.list.remove(integer);
				ite.remove();
			}
		}*/
		Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
            	synchronized (cme.list) {
					
            		Iterator<Integer> iterator = cme.list.iterator();
            		while (iterator.hasNext()) {
            			System.out.println("thread1 " + iterator.next());
            			try {
            				Thread.sleep(1000);
            			} catch (InterruptedException e) {
            				e.printStackTrace();
            			}
            		}
				}
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
            	synchronized (cme.list) {
					
            		Iterator<Integer> iterator = cme.list.iterator();
            		while (iterator.hasNext()) {
            			System.out.println("thread2 " + iterator.next());
            			iterator.remove();
            		}
				}
            }
        });
        thread1.start();
        thread2.start();
	}
}

 

使用同步的方式,可以解决上述的问题,但是会影响性能,尤其是当同步的执行的方法比较耗时,且在代码中再引入了其他的锁,可能会产生死锁的问题。可以使用线程封闭技术或者使用CopyOnwriteArraylist、CopyOnWriteArraySet“写入时复制”容器替代,但是这些也会引入新的问题,就是复制list或者set时也会有一些新的开销,这就要做出权衡,一般来说,当迭代操作远远多于修改操作的情况下才使用“写入时复制”的容器,感兴趣的可以尝试一下。

 

 

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/111248.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • 中位数和众数的定义_众数是什么意思中位数又是什么意思

    中位数和众数的定义_众数是什么意思中位数又是什么意思在初中数学课本中,我们学习了平均数,但是平均数与中位数、众数有是关系呐,下面我就为大家总结一下:平均数:是指在一组数据中所有数据之和再除以数据的个数。平均数是表示一组数据集中趋势的量数,它是反映数据集中趋势的一项指标。解答平均数应用题的关键在于确定“总数量”以及和总数量对应的总份数。在统计工作中,平均数(均值)和标准差是描述数据资料集中趋势和离散程度的两个最重要的测度值。平均数的分类:(1)算术平…

    2022年9月17日
    0
  • 后台 管理 系统

    后台 管理 系统课程/视频管理系统:https://item.taobao.com/item.htm?spm=a2oq0.12575281.0.0.50111deb1nO3pa&ft=t&id=643539140484教务管理系统:https://item.taobao.com/item.htm?spm=a2oq0.12575281.0.0.50111deb1nO3pa&ft=t&id=643539140484成绩管理系统:https://item.taobao.com/item.h

    2022年4月25日
    41
  • qi接收启动协议_基于QI协议的无线充电通信系统「建议收藏」

    qi接收启动协议_基于QI协议的无线充电通信系统「建议收藏」龙源期刊网http://doc.docsou.com基于QI协议的无线充电通信系统作者:胡江浩张中炜来源:《中国新通信》2016年第03期【摘要】无线充电技术的学名又叫做无线电能传输,其原理非常类似于变压器,都是通过发射电路产生一个交变电流通过初级线圈,从而在初级线圈上感应出一个交变电磁场,次级线圈通过接收该交变电磁场从而产生感应电流,通过电磁感应实现能源的传输。该文主要介绍了一种无线充电领域中…

    2022年6月16日
    29
  • 路由器刷机教程图解_小米路由器刷机教程[通俗易懂]

    路由器刷机教程图解_小米路由器刷机教程[通俗易懂]小米路由器刷机教程小米路由器刷机教程登陆路由器设置页面,刷新官方固件.使用路由器助手,自动检查并自动更新固件.使用u-boot模式,可刷新任何固件.小编温馨提示:你需要先在小米官网下载好相应固件到本地,再进入路由器设置,进入系统升级,选择下载好的固件进行系统升级….其他2016/06/10小米助手刷机教程小米助手刷机教程小米助手的刷机功能只能算是小米MIUI系统升级功能,如果无法开机…

    2022年7月21日
    50
  • IntelliJ IDEA Community Edition 社区版插件汇总「建议收藏」

    IntelliJ IDEA Community Edition 社区版插件汇总「建议收藏」一、前言今年Idea对盗版软件打击力度加大,朋友们会发现,旗舰版自己激活使用,过几天就会失效,需要重新激活,有的小伙伴就会选择去淘宝花钱买个教育邮箱注册,这个方法我使用过,过了两三个月就不能用了,着实让人头疼。如何解决呢?我想到了Idea社区版本,下载一个使用,将我的Springboot项目导入,启动下试试,不出所料,报错了。好啦!步入正题。社区版Idea相比旗舰版少了很多功能,包括Java开发最重要的Web开发能力!Spring项目没有Tomcat插件,不能在Idea启动。SpringBoot

    2022年9月15日
    0
  • webpack图片压缩_webpack怎么引入图片

    webpack图片压缩_webpack怎么引入图片图片处理url-loader(webpack5之前的处理方式)在项目开发中,我们时长会需要使用到图片,比如在img文件夹中有图片test1.png,然后在normal.css中会引用到图片body

    2022年7月31日
    4

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号