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)
上一篇 2021年8月2日 下午4:00
下一篇 2021年8月2日 下午5:00


相关推荐

  • vscode快捷键重置及快捷键恢复

    vscode快捷键重置及快捷键恢复在用vscode设置快捷键的时候,有的快捷键和自己设置的有重复和冲突现象,为了图方便我把与自己冲突的快捷键都删除了,结果导致键盘的删除按键用不了,相当于自己写的代码无法删除了。最后还是在官网上找到解决办法。 首先找到键盘快捷设置 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20191128152918849.png) ![在这里插入图片描述](http…

    2022年6月9日
    622
  • vue中显示静态图片怎么引用

    vue中显示静态图片怎么引用

    2022年4月2日
    45
  • 围观!一套开源车牌识别系统(附项目地址)

    围观!一套开源车牌识别系统(附项目地址)

    2020年11月13日
    247
  • PLC编程入门基础技术知识

    PLC编程入门基础技术知识PLC编程入门基础技术知识第一章可编程控制器简介可编程序控制器,英文称ProgrammableController,简称PC。但由于PC容易和个人计算机(PersonalComputer)混淆,故人们仍习惯地用PLC作为可编程序控制器的缩写。它是一个以微处理器为核心的数字运算操作的电子系统装置,专为在工业现场应用而设计,它采用可编程序的存储器,用以在其内部存储执行逻辑运算、顺序控制、定时/计数和算术运算等操作指令,并通过数字式或模拟式的输入、输出接口,控制各种类型的机械或生产过程。PLC是微机技术与

    2025年10月7日
    6
  • 亿能bms上位机_上位机软件 上位机PC软件 bms电池管理系统测试系统软件「建议收藏」

    亿能bms上位机_上位机软件 上位机PC软件 bms电池管理系统测试系统软件「建议收藏」上位机软件上位机PC软件bms电池管理系统测试系统软件上海鸣野软件开发公司从创立伊始就立足于差异化竞争,只做自己有竞争优势的事,13391389262,,在上位机软件,上位机开发,bms电池管理系统测试系统软件,上位机软件外包积累了丰富的开发经验,通过学习和创新,不断地扩大和加强公司在行业中的优势,是企业发展之道。关注别人所不关注的,看到别人所看不到的,做到别人所做不到的,是鸣野软件开发公司*…

    2022年5月31日
    60
  • acwing-2180. 最长递增子序列问题(最大流+拆点+最长上升子序列)

    acwing-2180. 最长递增子序列问题(最大流+拆点+最长上升子序列)给定正整数序列 x1,⋯,xn。计算其最长递增子序列的长度 s。计算从给定的序列中最多可取出多少个长度为 s 的递增子序列。(给定序列中的每个元素最多只能被取出使用一次)如果允许在取出的序列中多次使用 x1 和 xn,则从给定序列中最多可取出多少个长度为 s 的递增子序列。注意:递增指非严格递增。输入格式第 1 行有 1 个正整数 n,表示给定序列的长度。接下来的 1 行有 n 个正整数 x1,⋯,xn。输出格式第 1 行输出最长递增子序列的长度 s。第 2 行输出可取出的长度为 s 的

    2022年8月9日
    8

发表回复

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

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