List中subList方法抛出异常java.util.ConcurrentModificationException原理分析

简介: List中subList方法抛出异常java.util.ConcurrentModificationException原理分析

1、首先从测试代码开始:


public class Test {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0;i<6000;i++){
            list.add(i);
        }
        List<Integer> list1 = list.subList(0,3000);
        List<Integer> list2 = list.subList(3000,6000);
        list2.clear();
        System.out.println("list1 = " + list1);
    }
}


首先初始化一个6000个元素的list,然后,利用list.subList()截取3000个元素到list1中,再取出后3000个元素到list2中,然后清空list2,最后再打印list1,此时将抛出异常:


微信图片_20220111192555.png


2、前戏知识:

subList()方法原理分析:


上面的测试方式为什么会出现这个情况,看上去明明没有任何问题,但是打印list1的时候就抛出异常,肯定不可能是System.out.println()有bug吧,再来仔细看看代码,似乎只有打印语句前面几句话会出现问题,那么就是subList()的调用以及clear()这几句代码了,那么问题到底出现在哪里,我们来一探究竟;


接下来我们首先看一下ArrayList中对subList()方法的实现的源码,看它究竟干了些什么事儿:


微信图片_20220111192612.png


在subList()方法的源码中首先调用了 subListRangeCheck(fromIndex, toIndex, size) 这个方法主要作用就是判断subList()传入的参数是否合规,这里不是重点,重点在于它  return new SubList(this, 0, fromIndex, toIndex),返回了一个SubList对象,继续往下看一下这个SubList对象,源码在1010行:


微信图片_20220111192630.png

通过源码可以看到,这个SubList对象是一个内部类,


2.1、在构造对象时,会传入4个参数:


AbstractList<E> parent:当前调用subList()方法的list对象


int offset:偏移量(从0开始)


int fromIndex:开始下标(包含)


int toIndex:结束下标(不包含)


2.2、在构造器内部:


将传入的parent赋给SubList对象的成员变量parent;


fromIndex赋给SubList对象的成员变量parentOffset;


offset+fromIndex赋给SubList对象的成员变量offset,用于记录元素的偏移量;


toIndex - fromIndex赋给SubList对象的成员变量size,用于记录此时会返回的数据量大小;


最后一个是 ArrayList.this.modCount 赋给SubList对象的成员变量modCount ,这个赋值比较关键,记录了修改过的次数,默认为0;


到这里,构造一个SubList对象就完成了,你可能会有疑问,只是单纯的构造了一个SubList对象,那么是怎么进行赋值取值的;解决这个问题,来看一下SubList对象的get()方法:微信图片_20220111192659.png


在get()方法中,最终返回的是 ArrayList.this.elementData(offset + index);可以看到,它是从当前的ArrayList对象中维护的一个elementData()方法中取值,再来看elementData()这个方法:


微信图片_20220111192703.png


返回的是elementData这个数组中的元素:


微信图片_20220111192800.png


由此可见:SubList对象中操作的集合与原始list中操作的集合是同一个集合,通过offset偏移量加上index来标记元素的位置;所以,当你操作原始list或者截取元素后生成的list1集合,都是影响同一个集合。


3、高潮部分:

异常产生分析:


有了上面第二步的分析,有了一个基本认识,那就是list.subList()方法返回的集合会直接影响原始的list集合,接下来继续分析java.util.ConcurrentModificationException异常出现的原因;


再次回到测试代码的以下四句代码:


List<Integer> list1 = list.subList(0,3000);

List<Integer> list2 = list.subList(3000,6000);

list2.clear();

System.out.println("list1 = " + list1);

首先通过  List<Integer> list1 = list.subList(0,3000); 等到一个list1;


然后再次通过  List<Integer> list2= list.subList(3000,6000); 等到一个list2;


然后清空list2 即list2.clear();


最后打印:System.out.println("list1 = " + list1);


由于上面分析我们知道,list2调用clear()方法,那么此时原始list维护的底层elementData数组势必会受影响,具体就是会把这后面3000个元素给删除掉,此时list1再去打印,它会调用自己重写的迭代方法iterator()进行遍历,然后调用父级AbstractList的listIterator()方法,由于SubList类继承了AbstractList 所以它会来调用SubList类的listIterator(final int index)方法,此时该方法内部在第一句就调用了checkForComodification();这个方法:


微信图片_20220111192818.png


接下来看 checkForComodification()这个方法在干什么:


微信图片_20220111192834.png


重点来了,这个方法里面首先判断了 ArrayList.this.modCount 与 this.modCount(即SubList的modCount)是否相同,如果不相同则抛出异常java.util.ConcurrentModificationException,写得累死我了,绕了一大圈终于写到这个异常了,在生成list1时,它在实例化一个SubList对象时将原始list的modCount赋值给了SubList对象,此时是默认值0,当list2.clear()时,原始list的modCount已经发生了变化,即不再是0,所以 此时打印list1时,checkForComodification()方法中的ArrayList.this.modCount != this.modCount判断肯定时true,所以这就是异常抛出的原因。


4、附上一位研究了subList()方法上面的注释得出的结论的图供大家参考学习:


微信图片_20220111192851.png

相关文章
|
19天前
|
Java 调度
Java并发编程:深入理解线程池的原理与实践
【4月更文挑战第6天】本文将深入探讨Java并发编程中的重要概念——线程池。我们将从线程池的基本原理入手,逐步解析其工作过程,以及如何在实际开发中合理使用线程池以提高程序性能。同时,我们还将关注线程池的一些高级特性,如自定义线程工厂、拒绝策略等,以帮助读者更好地掌握线程池的使用技巧。
|
15天前
|
Java 调度
Java中常见锁的分类及概念分析
Java中常见锁的分类及概念分析
16 0
|
15天前
|
Java
Java中ReentrantLock中tryLock()方法加锁分析
Java中ReentrantLock中tryLock()方法加锁分析
13 0
|
1月前
|
开发框架 Java API
java反射机制的原理与简单使用
java反射机制的原理与简单使用
17 1
|
27天前
|
缓存 Java C#
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍(一)
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍
75 0
|
29天前
|
Java
软件工程设计原理里氏替换原则 ,具体实现及JAVA代码举例
里氏替换原则(Liskov Substitution Principle, LSP)是面向对象设计的基本原则之一,由Barbara Liskov提出。这个原则指出,如果类 S 是类 T 的子类型,则程序中使用 T 的对象的地方都可以不经修改地使用 S 的对象。换句话说,子类的对象应该能够替换掉它们的父类对象,而不影响程序的正确性。这个原则强调了继承关系中的行为兼容性,保证了基类和派生类之间的正确抽象和继承关系。
24 3
|
1月前
|
人工智能 监控 算法
java智慧城管源码 AI视频智能分析 可直接上项目
Java智慧城管源码实现AI视频智能分析,适用于直接部署项目。系统运用互联网、大数据、云计算和AI提升城市管理水平,采用“一级监督、二级指挥、四级联动”模式。功能涵盖AI智能检测(如占道广告、垃圾处理等)、执法办案、视频分析、统计分析及队伍管理等多个模块,利用深度学习优化城市管理自动化和智能化,提供决策支持。
223 4
java智慧城管源码 AI视频智能分析 可直接上项目
|
14天前
|
运维 NoSQL 算法
Java开发-深入理解Redis Cluster的工作原理
综上所述,Redis Cluster通过数据分片、节点发现、主从复制、数据迁移、故障检测和客户端路由等机制,实现了一个分布式的、高可用的Redis解决方案。它允许数据分布在多个节点上,提供了自动故障转移和读写分离的功能,适用于需要大规模、高性能、高可用性的应用场景。
16 0
|
15天前
|
Java
Java中关于ConditionObject的signal()方法的分析
Java中关于ConditionObject的signal()方法的分析
21 4
|
15天前
|
Java
Java中关于ConditionObject的分析
Java中关于ConditionObject的分析
18 3