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


相关推荐

  • pycharm创建django项目报错[通俗易懂]

    pycharm创建django项目报错[通俗易懂]ErrorcreatingDjangoapplication:Erroronpythonside.Exitcode如果创建Django项目的时候出现如上截图错误,则是Django下载失败,需要检查本机django是否下载成功,有的时候是因为缺少pytz包而导致django下载失败解决方案:关于pytz包缺失error:CouldnotfindsuitabledistributionforRequirement.parse(‘pytz’)首先去官网下载pytz包:http

    2025年6月15日
    7
  • string转换成jsonobject注意事项_cad如何转换成pdf格式

    string转换成jsonobject注意事项_cad如何转换成pdf格式在java中String类型的json转换成JSONObject代码如下:publicclassStringtoJson{ publicstaticvoidmain(String[]args){ Strings=”{a:1,b:2}”; JSONObjectjson1; try{ json1=newJSONObject(s);

    2022年8月23日
    8
  • 最新 Cocos2d-x 3.2 开发环境搭建(windows环境下)

    最新 Cocos2d-x 3.2 开发环境搭建(windows环境下)

    2021年12月5日
    57
  • OpenClaw高效使用指南:从精选技能安装到MCP配置的自动化办公实践

    OpenClaw高效使用指南:从精选技能安装到MCP配置的自动化办公实践

    2026年3月13日
    3
  • 学习Java大数据需要掌握哪些Java技能?

    学习Java大数据需要掌握哪些Java技能?学习Java大数据需要掌握哪些Java技能?现在大数据发展很速度很多小伙伴想要学习Java大数据技术开发,但是学习大数据为什么需要掌握Java技能呢?一、学大数据为什么要掌握Java?首先,我们学习大数据,为什么要先掌握Java技术?Java是目前使用非常广泛的编程语言,它具有的众多特性,特别适合作为大数据应用的开发语言。Java不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的

    2022年5月27日
    34
  • c语言中instr函数,InStr 函数

    c语言中instr函数,InStr 函数示例在表达式中使用 InStr 函数可以在任何可以使用表达式的地方使用 InStr 例如 如果要查找第一个周期 IP 地址的字段中 名为 IPAddress 可以使用 InStr 找到它 如下所示 InStr 1 IPAddress InStr 函数检查 IPAddress 字段中的每个值 并返回第一个周期的位置 因此 如果 IP 地址的第一部分为 10 该函

    2026年3月20日
    2

发表回复

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

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