并发修改异常ConcurrentModificationException详解(二)

简介: 并发修改异常ConcurrentModificationException详解

并发修改异常ConcurrentModificationException详解(一)https://developer.aliyun.com/article/1393271


继续Debug,我们记录一下几次hasNext()/next()方法时,其中几个重要变量值的变化过程。


第一次调用hasNext(): cursor = 0    size = 3
第一次调用iterator.next():
    第一次调用checkForComodification():modCount = 3  expectedModCount = 3
由于 modCount = expectedModCount ,不会发生并发修改异常。并且返回当前游标对应的值,即返回1.
由于满足Integer.parseInt(nextElement) < 2,所以会执行list.add("2")方法,之前已经了解到,add()方法会
修改modCount的值 + 1· 所以此时modCount的值变为4了.
第一次next()方法调用完,cursor游标的值会加一,所以cursor = 1. 
===============================================================================================================
第二次调用hasNext(): cursor = 1  size = 4
第二次调用iterator.next():
    第二次调用checkForComodification():modCount = 4  expectedModCount = 3
由于 modCount != expectedModCount ,此时会发生并发修改异常。
以上就是ConcurrentModificationException一场产生的简单解析过程。

下图是发生并发修改异常时checkForComodification()方法的执行过程,注意modCount和expectedModCount 的值:

image.png

四、并发修改异常的特殊情况

示例:已知集合中有三个元素:"chinese"、"math"、"english",使用迭代器进行遍历, 判断集合中存在"english",如果存在则删除。


public class TestConcurrentModifyException {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("chinese");
        list.add("math");
        list.add("english");
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String nextElement = iterator.next();
            if ("english".equals(nextElement)) {
                //使用ArrayList的boolean remove(Object o)方法进行删除
                list.remove("english");
            }
        }
    }
}

程序运行结果:


Exception in thread "main" java.util.ConcurrentModificationException
  at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
  at java.util.ArrayList$Itr.next(ArrayList.java:851)
  at com.wsh.springboot.helloworld.TestConcurrentModifyException.main(TestConcurrentModifyException.java:19)

通过上面的分析,由于往集合中加入了三个元素,所以modCount实际修改次数的值为3,当我们调用iterator()获取迭代器的时候,初始化expectedModCount的值也为3。下面我们一起看一下ArrayList类中的根据元素删除方法的源码。

remove(Object o)方法源码:


public boolean remove(Object o) {
    //判断需要删除的元素是否为null
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        //不为null,遍历集合,使用equals进行比较是否相等
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}
private void fastRemove(int index) {
    //删除元素时,实际修改次数会自增1
    //此时: modCount实际修改次数为4,但是预期修改次数还是获取迭代器时候的3,两者已经不一致了。
    modCount++;
    //计算集合需要移动元素的个数
    int numMoved = size - index - 1;
    if (numMoved > 0)
        //数组拷贝
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    //将删除元素置为null,利于垃圾回收
    elementData[--size] = null; // clear to let GC do its work
}

我们分析一下程序的执行过程,查看并发修改异常是怎么产生的。当我们执行到下面一行语句之后,集合的size会减1,所以此时size = 2.


list.remove("english");

那么这时候再次执行下面的判断


while (iterator.hasNext()) {

此时cursor的值是3,但是size的值是2,两者不相等,所以hasNext()方法返回true,意味着集合中还有元素,所以还会执行一次next()方法,此时执行checkForComodification()方法,判断modCount是否等于expectedModCount,(expectedModCount=3, modCount=4),两者不相等,所以这就抛出了并发修改异常。

小结论:

  1. 集合每次调用add方法时,实际修改次数的值modCount都会自增1;
  2. 在获取迭代器的时候,集合只会执行一次将实际修改集合的次数modCount的值赋值给预期修改的次数变量expectedModCount;
  3. 集合在删除元素的时候,也会针对实际修改次数modCount的变量进行自增操作;

下面再来看一个并发修改异常的特殊情况,观察下面的程序:

示例:已知集合中有三个元素:"chinese"、"math"、"english",使用迭代器进行遍历,判断集合中存在"math",如果存在则删除。


public class TestConcurrentModifyException {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("chinese");
        list.add("math");
        list.add("english");
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String nextElement = iterator.next();
            if ("math".equals(nextElement)) {
                //使用ArrayList的boolean remove(Object o)方法进行删除
                list.remove("math");
            }
        }
        System.out.println(list);
    }
}

我们可以看到,这个示例跟上面一个实例非常相似,唯一不同的是这次删除的元素是集合中的倒数第二个元素。

程序运行结果:


[chinese, english]

我们看到,这里并没有发生并发修改异常,很神奇,而且成功删除”math“这个元素,这是为什么呢?上面一个示例明明说了会发生并发修改异常。下面我们还是分析一下其中的特殊原因:


第一次调用hasNext(): cursor = 0    size = 3, hasNext()返回true
第一次调用iterator.next():
    第一次调用checkForComodification():modCount = 3  expectedModCount = 3
由于 modCount = expectedModCount ,不会发生并发修改异常。
第一次next()方法调用完,cursor游标的值会加一,所以cursor = 1. 
===============================================================================================================
第二次调用hasNext(): cursor = 1  size = 3, hasNext()返回true
第二次调用iterator.next():
    第二次调用checkForComodification():modCount = 3  expectedModCount = 3
由于 modCount = expectedModCount ,不会发生并发修改异常。
第二次next()方法调用完,cursor游标的值会加一,所以cursor = 2. 
由于上面的示例中,"math"元素刚好在第二个,所以这时候"math".equals(nextElement)会返回true,
所以会执行集合的删除元素方法,size会减一,实际修改次数modCount会加一,所以size = 2  modCount = 4
ps:这里注意cursor游标的值也是2,size的值也是2,
===============================================================================================================
第三次调用hasNext(): cursor = 2  size = 2, 两者相等,所以hasNext()返回false,while循环结束,意味着不会调用next()方法,
不会执行调用checkForComodification()方法,那么肯定就不会发生并发修改异常。

小结论:

  1. 当要删除的元素在集合中的倒数第二个元素的时候,删除元素不会产生并发修改异常。
  2. 原因:因为在调用hasNext()方法的时候,cursor = size是相等的,hasNext()方法会返回false, 所以不会执行next()方法,也就不会调用checkForComodification()方法,就不会发生并发修改异常。

五、如何避免并发修改异常?

如何避免并发修改异常还有它的特殊情况呢,其实Iterator迭代器里面已经提供了remove(),用于在迭代过程对集合结构进行修改,使用iterator.remove()不会产生并发修改异常,为什么迭代器的删除方法不会产生异常呢,我们得去看看Itr内部类的remove()源码:


//迭代器自带的删除方法
public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    //校验是否产生并发修改异常    
    checkForComodification();
    try {
          //真正删除元素的方法还是调用的ArrayList的删除方法
         //根据索引进行删除
         ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        //每次删除完成后,会重新将expectedModCount重新赋值,值就是实际修改次数modCount的值
        //这就保证了,实际修改次数modCount一定会等于预期修改次数expectedModCount ,所以不会产生并发修改异常.
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

小结论:

  1. 迭代器调用remove()方法删除元素,底层还是调用的集合的删除元素的方法;
  2. 在调用remove()方法后,都会将modCount的值赋值给expectedModCount,保证了它两的值永远都是相等的,所以也就不会产生并发修改异常;

六、总结

以上通过几个示例讲解了并发修改异常的现象,以及分析了并发修改异常是如何产生的,在实际工作中,如果需要使用到删除集合中元素,那么我们不要使用集合自带的删除方法,我们应该使用iterator迭代器给我们提供的删除方法,这样可以很大程序避免程序发生并发修改异常ConcurrentModificationException。

相关文章
|
1月前
|
存储 安全 Java
如何避免`ArrayStoreException`异常?
`ArrayStoreException`是在Java中尝试将错误类型的对象存储到泛型数组时抛出的异常。要避免此异常,需确保向数组添加的对象类型与数组声明的类型一致,使用泛型和类型检查,以及在运行时进行类型安全的转换和验证。
|
2月前
|
缓存 Linux C++
map异常崩溃分析汇总
文章讨论了std::map和std::set在某些情况下崩溃的原因,包括结构体字节对齐问题、多线程资源同步问题、以及比较器的实现问题,并提供了相应的解决方案。
|
3月前
|
安全 测试技术 数据库连接
如何避免 C# 中的异常
【8月更文挑战第27天】
50 2
|
3月前
|
Java
线程池中线程抛了异常,该如何处理?
【8月更文挑战第27天】在Java多线程编程中,线程池(ThreadPool)是一种常用的并发处理工具,它能够有效地管理线程的生命周期,提高资源利用率,并简化并发编程的复杂性。然而,当线程池中的线程在执行任务时抛出异常,如果不妥善处理,这些异常可能会导致程序出现未预料的行为,甚至崩溃。因此,了解并掌握线程池异常处理机制至关重要。
440 0
|
6月前
并发修改异常ConcurrentModificationException详解(一)
并发修改异常ConcurrentModificationException详解
126 0
并发修改异常ConcurrentModificationException详解(一)
|
存储 Go
当map在不提前分配内存的时候为什么会抛异常?
当map在不提前分配内存的时候为什么会抛异常?
如何处理 JDK 线程池内线程执行异常
如何处理 JDK 线程池内线程执行异常
136 2
你真的明白关于迭代器的方法、使用异常、并发修改异常介绍嘛?
关于迭代器的方法、使用异常、并发修改异常介绍的使用
137 0
你真的明白关于迭代器的方法、使用异常、并发修改异常介绍嘛?
|
存储 安全 Java
【HashMap并发修改异常】
【HashMap并发修改异常】
388 0