一个ConcurrentModificationException异常引发的故事

一、问题背景

 最近在分析一个客户的实际问题中,从日志中看到了一个类似如下的异常(图A),实际异常栈信息量较多,由于涉及到产品代码,所以不便在此贴出,图A是异常栈的最底层抛异常的原因;程序并没有对最上层那行业务代码做try{}catch(),而该功能是会与第三方系统交互的,在最开始设计该功能时是分两大块处理的,每块都有一个大的事务管理;因此该异常直接跑飞没能反写第一个事务已提交的数据,导致该业务数据对象状态不正确,从而只能通过在后台修改让业务人员在重新操作,实际业务场景不便在此赘述。一个ConcurrentModificationException异常引发的故事_第1张图片 

     图A

 

二、要解决的问题

    问题本身是比较容易解决的,catch处后进行反写操作,在抛  出异常即可,而真正要弄明白的问题是

  1、这个异常是什么、通常是什么场景下可能会出现?

2、该异常是否真的无法重现?

3、抛该异常的本质原因是什么?抛这个异常的逻辑是?

4、能否用一段小程序通过调试的方式模拟重现?

5、产品当时并发是满足了哪些因素,程序走到哪,进行了何种操

作这么巧发生了?


三、解决疑问


   (1)这个异常是什么、通常是什么场景下可能会出现?

 百度可知该异常已屡见不鲜,不管是单线程还是多线程的情况都有,比如单线程下一个实际类型为ArrayList的集合list里面添加了5个元素,现对list进行迭代,中间做一个if判断,判断条件如迭代到第3个元素时,通过list.remove(obj)方式移除对象,在继续迭代时就会报该异常这种场景是由代码编写有误导致,正确做法应该是通过迭代器移除对象

 而多线程的场景,简单看了其中一个示例则是把list定义为静态属性,被不同线程操作并发导致的,比如一个线程正在对这个list做迭代操作,另一个线程通过list在做remove操作,这种情形也是有可能发生的 

 

(2)该异常是否真的无法重现?

 分析日志后,已可以准确判断是需要做数据修复的,修改其中一笔后,重新操作ok了,后将这些笔数据状态修改后批量一次操作也是ok的,说明这是偶发现象,因为平常客户使用时也一直没有问题。

 基于该异常的场景及该功能后台代码的分析,可以肯定是并发导致。

    为啥这么肯定?是不是看错日志了?

  1、通过客户端操作日志,服务端日志和后台数据发生时间,这三者的时间是极其吻合的并且报的异常栈      信息也是对应这个功能的,从业务者的角度操作 该业务数据对象,现象也是相符的  

  2、当时客户操作的笔数是33笔,大于10笔客户端会启动多个线程的

3、日志已铁证如山的表明当时抛了异常

     可以想象当时一个线程在迭代HashMap中的元素时,另外一个线程对  HashMap做了某个操作导致,比如   remove操作;因此根据异常堆栈中涉及的流程性代码,仔细的检查了一遍,并没有把map定义为全局变量,也   没有remove操作,只看到了list.addAll(>),并且Map.Entry不   是静态的,而是一个典型的成员变量.

     因此需要看HashMap的源码仔细分析抛该异常的本质原因,是具体满足了哪些因素后才会抛该异常。


  (3)抛该异常的本质原因是什么?抛这个异常的逻辑是?

    查看HashMap相关源码可知,当迭代器迭代的时,会判断modCount的值和expectedModCount的值是否相    等;如果相等则往下进行,如果不等则抛该异常,因此该异常的本质原因是这两个值不等导致的。

    那么modCount和expectedModCount这两个值是在哪定义的呢?什么时候发生变化的呢?

 分析调试源码知:modCount是HashMap中的成员变量,expectedModCount是HashIterator中的一个成员变量,并且HashIterator是HashMap的内部类;搜索modCount在HashMap中引用可知,put、remove等操会让modCount的值发生变化,而且变化指的就是自加1,从英文的表面意思上可以猜出这个变量设计的业务语义是表示HashMap修改的次数,所以只要HashMap这个容器往里放对象或者是移除对象导致HashMap结构发生变化的操作都会使modCount的值就会加1,而迭代操作不会。

 expectedModCount的值则是在初始化迭代器对象HashIterator时,在构造函数就进行了modCount = expectedModCount的赋值,其次,是在使用迭代器移除map中的元素时又进行了赋值动作,在方法的结尾会将modCount赋值给expectedModCount,因为HashIterator迭代器移除元素的具体实现就是通过调用了HashMap的移除方法,此时modCount会+1,因此对expectedModCount进行了重新赋值;而这是为什么在迭代过程中通过迭代器移除map元素不会报此异常,通过map本身的方式移除元素会报此异常的原因。


 (4)能否用一段小程序通过调试的方式模拟重现

  已模拟出,具体代码不在此贴出了。


四、场景还原


  通过上述分析和模拟程序可知,当时抛该异常应该同时满足如下3点:

  1、必须操作的是同一个map对象

  2、其中一个线程肯定在执行到了判断modCount值与 expectedModCount是否相等此处(日志可知此时是在做迭 代操作,此条件已满足)

  3、另外一个线程同样拿到该map对象进行了put,或者remove操作,导致modCount值发生了变化


   业务代码并没有写,为什么会有迭代操作?

     从日志分析可知是list在addAll时产生的,这是sun实现addAll方法时产生的。

  查看ArrayList中的addAll(Collection c)可知,实现Collection接口的类的对象都可以作为参数传入该方法   中,在ArrayList的addAll方法中,首先是要将c转换成一个对象数组,Collection接口定义了该方法       toArray(),让实现类去实现,在运行期在去真正调用实现类的该方法,通过日志可知,我们传进去的对象是   HashMap的内部类EntrySet>对象,而EntrySet是实现了Collection接口的,事实   上内部类EntrySet本身没有重写toArray()方法,而是他继承的父类的父类AbstractCollection实现了该方     法,   具体如图B;在AbstractCollection的toArray方法中首先是创建了一个数组对象,而其本身定义了一   个获取迭   代器的抽象方法iterator,在运行期动态调用真正子类的iterator创建迭代器,具体如图C

    一个ConcurrentModificationException异常引发的故事_第2张图片

                            图B(AbstractCollecton实现toArray)

  

                             图C(子类创建迭代器)

  继续看源码可知,newEntryIterator 是创建了一个EntryIterator的迭代器对象,完成其父类和自己的初   始化工作,而EntryIterator是继承自HashIterator的,并且这两个类都是HashMap的内部类,HashIterator实   现了Iterator接口,因此在初始化EntryIterator时就连带着初始化HashIterator并且是先初始化         HashIterator;在初始化HashIterator时expectedModCount就被赋值了,取的是当前HashMap对象的         modCount,modCount的值此时不一定是0,假如该对象已经put了2个值,那么此时              expectedModCount=modCount=2,如expectedModCount初始化被赋值代码如图D

一个ConcurrentModificationException异常引发的故事_第3张图片 

                              图D

方法调用栈如图E

一个ConcurrentModificationException异常引发的故事_第4张图片

                                                                    

                             图E                                             

说明:是对象初始化方法,是存放静态属性或者方法块的方法,这两个方法是编译器在编译的时候产生的,具体是什么条件下产生的在此就不在赘述了

     AbstractCollection拿到迭代器后,就开始迭代,将HashMap中的元素一个一个迭代出来保存到对象数组中,所以此时另外一个线程拿到该map对象,正好进行put操作,此时就抛异常了。

通过上述分析可以明白,list.addAll为啥会产生知迭代操作了。


那么是如何传同一个map对象的呢?又是在什么时候进行put操作的呢?

   分析调试相关基础框架代码可知,是同一个上下文导致了同一个map;基础框架中通过SessionManager   来管理SessionInfo,存放SessionInfo的容器实际上是一个final static HashMap,不同   线程则是通过该对象进行信息交换和传递的;而上下文context是作为SessionInfo的成员变量存在的,并且用   final进行修饰了;而我们最终关心的entrySet对应的Map则来于Context的一个普通的成员变量;因此不同线   程通过同一个sessionid就可以拿到同一个session,最终操作同一个map对象了。

  经实验(调试)可知,在不退出客户端的情况下,sessionid是相同的,并且多线程提交到服务端后取到   的sessionid,sessionInfo对象也是相同的,此处并没有深究sessionid为啥是相同的,但从业务语义来讲在   客户端未关闭前,每次拿到的sessionid是一样的是比较合理。

  分析至此,离真相越来越近,下面则是什么时候进行的put操作。

 从Context里可知有一个put方法,而该方法是在unmarashel中调用,unmarshel则又是通过rpc调用远程方   法    的时候调用,根据日志的异常栈信息可知,当时确实进行了远程调用,所以当时是一个线程调用远程   方法后进行addAll中的迭代操作时,另一个线程也在调用远程方法进行unmarashel的过程中进行了put操作,   导致并发产生,出现了该异常。

一切都是那么不可思议,但确实又发生了。

 

                        

你可能感兴趣的:(java源码)