并发修改异常(ConcurrntModificationException)原理及其解决方案

阅读 92

2022-03-12

并发修改异常 (ConcurrntModificationException)原理及其解决方案

问题演示

小伙伴们可以运行一下

遍历集合元素的过程中删除元素
public void testConcurrentModifyException() throws InterruptedException {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(1);
        for(Integer num : list){
            list.remove(0);
        }
    }
第二种方式
1.创建10个线程分别共享变量ArrayList添加元素,
2.主线程对ArrayList进行遍历
运行过程中就会抛出ConcurrentModificationException
public void testConcurrentModifyException() throws InterruptedException {
        List<Integer> list = new ArrayList<>();
        for(int i = 0;i < 10;i++){
            new Thread(()-> {
                for(int j = 0;j < 100;j++) {
                    list.add(j);
                }
            },"A + i").start();
        }
        for(Integer tmp:list){}
        Thread.sleep(20000);
        System.out.println(list.size());
    }

其实这两种方式触发ConcurrentModificationException的通过相同的方式,看似一个单线程,一个多线程

我在这个地方简述一下这个异常出发的大致原理;
1.首先ArrayList类中有一个共享变量modCount,我们每操作一次集合这个变量都会进行加一操作(增,删,改,不包含查询)
2. 我们在开始遍历集合元素的时候,会记录此时的modCount作为expectedModCount,每次遍历一个元素的的时候都会检查,这两个参数是否相等;如果不想等就会抛出ConcurrentModifyException

第一种方式解决方式是比较简单的

下面是代码示例:

public void testSolveModificationException () {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(1);
        Iterator<Integer> it = list.iterator();
        while(it.hasNext()) {
            Integer num = it.next();
            System.out.println(num);
            it.remove();
        }
        System.out.println("size = " + list.size());
    }

为什么这个方法没有抛出并发修改异常?大概浏览一下代码,迭代器删除元素是,调用也是ArrrayList对象的remove, 此时成员变量modCount进行++操作,此时迭代器用modCount对给expectModCount进行赋值,所以通过这个方式不会抛出并发修改异常

第二种情况的解决方式:

我们可以分析思路:

出现并发修改异常,多个线程对共享变量ArrayList并发进行读写导致的,一般需要用到锁相关的知识了,我们是无法直接对ArrayList进行加锁的

解决方式

1:使用线程安全的集合(Vector),

这是从jdk 1.1开始就存在的集合,其实也会抛并发修改异常,跟进了一下源码,所有的方法都加了synchronized,为什么还会出现异常呢?这是因为 遍历元素时,各个元素都是独立的释放获取锁,这也造成 增加,遍历元素,交替的获取锁的所以此时调用checkForComodification也会抛ConcurrentModificationException;

2.使用jdk提供的提供的工具类,

但是下面这种方式,并不能解决并发并发修改异常,小伙伴可以多实验几次,就会复现;其实jdk工具类的实现的基本逻辑是基于装饰的设计模式;装饰之后是实现类SynchronizedRandomAccessList;其实使用这种方式,只是增,删,改,编程进行了加锁,对于遍历是没有加锁的 源码了只有 Must be manually synched by user 大概意思是遍历需要用户自己实现同步;
虽然是一个大坑,但是我们开发过程中很少使用,也就很少遇到了;

List list = Collections.synchronizedList(new ArrayList<>())

public void testConcurrentModifyExceptionV2() throws InterruptedException {
        //List<Integer> list = new ArrayList<>();
        List<Integer> list = Collections.synchronizedList(new ArrayList<>());
        for(int i = 0;i < 10;i++){
            new Thread(()-> {
                for(int j = 0;j < 100;j++) {
                    list.add(j);
                }
            },"A + i").start();
        }
        int cnt = 0;
        for(Integer tmp:list){
            cnt++;
        }
        System.out.println(cnt);
        Thread.sleep(20000);
        System.out.println(list.size());
    }

第三种方式 CopyOnWriteArrayList,传说中的写时复制,基本思想有点类似于读写分离,随后会针对CopyOnWriteArrayList写一个文章,这里就不再介绍了

欢迎小伙们多多指正,共同进步;

精彩评论(0)

0 0 举报