ConcurrentModificationException 的根源分析

本文深入解析了ConcurrentModificationException异常的产生原因,并提供了避免该异常的解决方案。通过代码示例展示了单线程环境下如何触发这一异常,同时探讨了集合操作中的隐式迭代问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

<!-- /* Font Definitions */ @font-face {font-family:宋体; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-alt:SimSun; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 135135232 16 0 262145 0;} @font-face {font-family:"Cambria Math"; panose-1:2 4 5 3 5 4 6 3 2 4; mso-font-charset:0; mso-generic-font-family:roman; mso-font-pitch:variable; mso-font-signature:-1610611985 1107304683 0 0 159 0;} @font-face {font-family:Calibri; panose-1:2 15 5 2 2 2 4 3 2 4; mso-font-charset:0; mso-generic-font-family:swiss; mso-font-pitch:variable; mso-font-signature:-1610611985 1073750139 0 0 159 0;} @font-face {font-family:"Comic Sans MS"; panose-1:3 15 7 2 3 3 2 2 2 4; mso-font-charset:0; mso-generic-font-family:script; mso-font-pitch:variable; mso-font-signature:647 0 0 0 159 0;} @font-face {font-family:"/@宋体"; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 135135232 16 0 262145 0;} /* Style Definitions */ p.MsoNormal, li.MsoNormal, div.MsoNormal {mso-style-unhide:no; mso-style-qformat:yes; mso-style-parent:""; margin:0cm; margin-bottom:.0001pt; text-align:justify; text-justify:inter-ideograph; mso-pagination:none; font-size:10.5pt; mso-bidi-font-size:11.0pt; font-family:"Calibri","sans-serif"; mso-fareast-font-family:宋体; mso-bidi-font-family:"Times New Roman"; mso-font-kerning:1.0pt;} .MsoChpDefault {mso-style-type:export-only; mso-default-props:yes; font-size:10.0pt; mso-ansi-font-size:10.0pt; mso-bidi-font-size:10.0pt; mso-ascii-font-family:Calibri; mso-fareast-font-family:宋体; mso-hansi-font-family:Calibri; mso-font-kerning:0pt;} /* Page Definitions */ @page {mso-page-border-surround-header:no; mso-page-border-surround-footer:no;} @page Section1 {size:841.9pt 595.3pt; mso-page-orientation:landscape; margin:1.0cm 1.0cm 1.0cm 1.0cm; mso-header-margin:42.55pt; mso-footer-margin:49.6pt; mso-paper-source:0; layout-grid:15.6pt;} div.Section1 {page:Section1;} -->

并发修改异常是一个在系统中很常见的异常,但是很多开发人员对这个异常有误解,一定认为是在并发修改时修改产生该异常,什么是并发修改了,就是在多线程中修改,但是在检查自己带码时候并没有使用到多线程,所以就一直找不到问题根源,根源请参见下面。。。。

 

ConcurrentModificationException 的解释

      我们可以查看 JDKAPI 来参考该异常的解释 当方法检测到对象的并发修改时,抛出此异常。

 

我门系统中大部分在是在使用 Collection 上进行迭代时抛出异常,但是我们系统都是使用的单线程,为什么会发生该异常了

我们参考下面错误代码,一个简单的需求,用户获取一个集合数据,需要讲不满足用户需求的数据删除掉

 

代码:

// 从业务方法中查询数据

           List<String> list = service .queryResult();

           for (String value : list)

           {

                 //bbb 数据不是用户需要的数据,我们需要移除

                 if (value.equals( "aaa" ))

                 {

                       // 将不满足用户数据删除

                      list.remove(value);

                 }

           }

  运行结果 :

       Exception in thread "main" java.util.ConcurrentModificationException

                at java.util.AbstractList$Itr.checkForComodification( AbstractList.java:372 )

                at java.util.AbstractList$Itr.next( AbstractList.java:343 )

                at com.demo.ConcurrentExceptionDemo.main( ConcurrentExceptionDemo.java:24 )

我们可以发现上面的代码并没有什么并发问题,到底是什么原因发生该异常,我们可以再次查看 API 文档

  引用文档上面的话:“ 此异常不会始终指出对象已经由不同 线程并发修改。如果单线程发出违反对象协定的方法调用序列,则该对象可能抛出此异常”现在我们知道了单线程也是会抛出该异常的,由于我们违反了对象的协定的方法调用序列,但是我们是在哪里违反了协定了?我们再次查看 API 文档上面发现 : “行该操作的迭代器称为快速失败 迭代器,因为迭代器很快就完全失败,而不会冒着在将来某个时间任意发生不确定行为的风险”,这句话什么意思了?看了很久没有搞明白,所以直接查看源代码发现:、

 

final void checkForComodification () {

          if ( modCount != expectedModCount )

           throw new ConcurrentModificationException();

      }

}

我们使用的迭代器在调用 next ()调用了改方法 checkForComodification () ,当每次检查 modCount != expectedModCount 就会抛出该异常,

下面我们来再次分析我们的代码

modCount 是集合每次在调用 remove add 方法时都会记录修改次数 modCount++ 但是我们迭代产生后 expectedModCount 就是一个固定值了,所以你只要在迭代时修改了集合就会造成 modCount != expectedModCount ,所以就抛出了 ConcurrentModificationException

 

现在我们应该已经搞清楚了改异常产生的原因了,但是我们还是要实现上面的需求,不满足要求还是要删除,我们代码该怎么实现了

方案一 : 通过调用迭代器的 remove 方法来实现删除

Iterator<String> ite = list.iterator();

           while (ite.hasNext())

           {

                 //aaa 数据不是用户需要的数据,我们需要移除

                 if (ite.next().equals( "aaa" ))

                 {

                      list.remove( "aaa" );

                 }

           }

我们来看看为什么调用迭代器的 remove 方法不会发生该异常了,下面是该迭代器的 remove 方法源码

public void remove() {

          if ( lastRet == -1)

           throw new IllegalStateException ();

            checkForComodification();

          try {

           AbstractList. this .remove( lastRet );

           if ( lastRet < cursor )

               cursor --;

           lastRet = -1;

           expectedModCount = modCount;

          } catch (IndexOutOfBoundsException e) {

           throw new ConcurrentModificationException();

          }

      }

我们发现上面加粗标红的字体就应该明白原因了吧。

方案二 : 首先获取需要删除的元素,然后通过迭代要删除的元素来删除

           List<String> delResults = new ArrayList<String>();

           for (String value : list)

           {

                 //bbb 数据不是用户需要的数据,我们需要移除

                 if (value.equals( "bbb" ))

                 {

                      delResults.add(value);

                 }

           }

           // 然后迭代要删除的集合来删除

           for (String delValue : delResults)

           {

                 list.remove(delValue);

           }

这样也是没有问题的,但是我们发现方案二好像比方法一多做了线性循环。

 

 

集合抛出 ConcurrentModificationException 进阶

我们下面来看一下下面代码

package com.demo;

import java.util.HashSet;

import java.util.Random;

import java.util.Set;

import java.util.concurrent.SynchronousQueue;

public class HiddenIterator

{

    private Set<Integer> set = new HashSet<Integer>();

    public void add(Integer i)

    {

        set.add(i);

    }

    public void remove(Integer i)

    {

        set.remove(i);

    }

    public void addTenThings()

    {

        Random rand = new Random();

        for (int i =0;i<10;i++)

        {

            add(rand.nextInt());

            System.out.println("DEBUG add ten elements to set : " + set);

        }

    }

    public static void main(String []args)

    {

        final HiddenIterator hi = new HiddenIterator();

        new Thread(){

            public void run() {

                hi.addTenThings();

            };

        }.start();

        hi.addTenThings();

    }

}

 

我们来看看改代码,暂时不要管上面标红的行 ,就是在添加了一个元素打印集合中的值 ,我们看看程序的原意就是通过两个线程来向集合中添加元素,运行该代码发现下面结果

Exception in thread "Thread-0" java.util.ConcurrentModificationException

      at java.util.HashMap$HashIterator.nextEntry( HashMap.java:793 )

      at java.util.HashMap$KeyIterator.next( HashMap.java:828 )

      at java.util.AbstractCollection.toString( AbstractCollection.java:421 )

      at java.lang.String.valueOf( String.java:2826 )

      at java.lang.StringBuilder.append( StringBuilder.java:115 )

      at com.demo.HiddenIterator.addTenThings( HiddenIterator.java:28 )

      at com.demo.HiddenIterator$1.run( HiddenIterator.java:37 )

 

我们上面已经知道集合是在做迭代集合的时候才会发生该异常,我们可以看集合的 add() 方法里也完全没有抛出该异常的代码。这就百思不得其解了,然后一句一句的代码查看,原来发现问题原因处在了我们上面标红的代码行。我们其实只要注释掉打印语句就不会发生该异常了

原来我们打印集合时,会自动调用 toString ()方法,我们现在查看 toString ()方法源码

public String toString () {

        Iterator<E> i = iterator();

      if (! i.hasNext())

          return "[]" ;

 

      StringBuilder sb = new StringBuilder();

      sb.append( '[' );

      for (;;) {

          E e = i.next();

          sb.append(e == this ? "(this Collection)" : e);

           if (! i.hasNext())

           return sb.append( ']' ).toString();

          sb.append( ", " );

      }

原来在调用集合 toString 方法时做了集合的迭代操作,所以导致了改异常的产生,因此我们在使用集合时要注意集合的一些隐式迭代操作,整理了一下集合的 hashCode,equals,toString,containsAll,removeAll,retainAll 方法都做了隐式迭代,所以我们在并发环境下将集合作为另一个容器的元素和一个容易的 key 时要注意,以及把容器作为参数的构造函数时都会对容器进行迭代,因此在这些操作下都可能会引起 ConcurrentModificationException

 

 

      

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值