Java ConcurrentModificationException 异常分析与解决方案
from: https://www.2cto.com/kf/201403/286536.html
Java ConcurrentModificationException 异常分析与解决方案

一、单线程
1. 异常情况举例
只要抛出出现异常,可以肯定的是代码一定有错误的地方。先来看看都有哪些情况会出现ConcurrentModificationException异常,下面以ArrayList remove 操作进行举例:
使用的数据集合:
| 1 2 3 4 5 6 7 | List<string> myList = newArrayList<string>();myList.add( "1");myList.add( "2");myList.add( "3");myList.add( "4");myList.add( "5");</string></string> | 
以下三种情况都会出现异常:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | Iterator<string> it = myList.iterator(); while(it.hasNext()) {     String value = it.next();      if(value.equals( "3")) {          myList.remove(value);  // error     }} for(Iterator<string> it = myList.iterator(); it.hasNext();) {     String value = it.next();      if(value.equals( "3")) {          myList.remove(value);  // error     }} for(String value : myList) {     System. out.println( "List Value:"+ value);      if(value.equals( "3")) {          myList.remove(value);  // error     }}   </string></string> | 
异常信息如下:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.AbstractList$Itr.checkForComodification(Unknown Source)
at java.util.AbstractList$Itr.next(Unknown Source)
2. 根本原因
以上都有3种出现异常的情况有一个共同的特点,都是使用Iterator进行遍历,且都是通过ArrayList.remove(Object) 进行删除操作。
想要找出根本原因,直接查看ArrayList源码看为什么出现异常:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | publicclassArrayList<e> extendsAbstractList<e>        implementsCloneable, Serializable, RandomAccess {                         @Overridepublicbooleanremove(Object object) {        Object[] a = array;        ints = size;        if(object != null) {            for(inti = 0; i < s; i++) {                if(object.equals(a[i])) {                    System.arraycopy(a, i + 1, a, i, --s - i);                    a[s] = null;  // Prevent memory leak                    size = s;                    modCount++;  // 只要删除成功都是累加                    returntrue;                }            }        } else{            for(inti = 0; i < s; i++) {                if(a[i] == null) {                    System.arraycopy(a, i + 1, a, i, --s - i);                    a[s] = null;  // Prevent memory leak                    size = s;                    modCount++;  // 只要删除成功都是累加                    returntrue;                }            }        }        returnfalse;    }       @OverridepublicIterator<e> iterator() {        returnnewArrayListIterator();    }               privateclassArrayListIterator implementsIterator<e> {          ......             // 全局修改总数保存到当前类中        /** The expected modCount value */        privateintexpectedModCount = modCount;        @SuppressWarnings("unchecked") publicE next() {            ArrayList<e> ourList = ArrayList.this;            intrem = remaining;               // 如果创建时的值不相同,抛出异常            if(ourList.modCount != expectedModCount) {                thrownewConcurrentModificationException();            }            if(rem == 0) {                thrownewNoSuchElementException();            }            remaining = rem - 1;            return(E) ourList.array[removalIndex = ourList.size - rem];        }                     ......     }}   </e></e></e></e></e> | 
List、Set、Map 都可以通过Iterator进行遍历,这里仅仅是通过List举例,在使用其他集合遍历时进行增删操作都需要留意是否会触发ConcurrentModificationException异常。
3. 解决方案
上面列举了会出现问题的几种情况,也分析了问题出现的根本原因,现在来总结一下怎样才是正确的,如果避免遍历时进行增删操作不会出现ConcurrentModificationException异常。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | // 1 使用Iterator提供的remove方法,用于删除当前元素 for(Iterator<string> it = myList.iterator(); it.hasNext();) {     String value = it.next();      if(value.equals( "3")) {          it.remove();  // ok     }}System. out.println( "List Value:"+ myList.toString()); // 2 建一个集合,记录需要删除的元素,之后统一删除             List<string> templist = newArrayList<string>(); for(String value : myList) {      if(value.equals( "3")) {          templist.remove(value);     }} // 可以查看removeAll源码,其中使用Iterator进行遍历myList.removeAll(templist);System. out.println( "List Value:"+ myList.toString());          // 3. 使用线程安全CopyOnWriteArrayList进行删除操作List<string> myList = newCopyOnWriteArrayList<string>();myList.add( "1");myList.add( "2");myList.add( "3");myList.add( "4");myList.add( "5");Iterator<string> it = myList.iterator(); while(it.hasNext()) {     String value = it.next();      if(value.equals( "3")) {          myList.remove( "4");          myList.add( "6");          myList.add( "7");     }}System. out.println( "List Value:"+ myList.toString()); // 4. 不使用Iterator进行遍历,需要注意的是自己保证索引正常 for( inti = 0; i < myList.size(); i++) {     String value = myList.get(i);     System. out.println( "List Value:"+ value);      if(value.equals( "3")) {          myList.remove(value);  // ok          i--; // 因为位置发生改变,所以必须修改i的位置     }}System. out.println( "List Value:"+ myList.toString());</string></string></string></string></string></string> | 
输出结果都是:List Value:[1, 2, 4, 5] , 不会出现异常。
以上4种解决办法在单线程中测试完全没有问题,但是如果在多线程中呢?
二、多线程
1. 同步异常情况举例
上面针对ConcurrentModificationException异常在单线程情况下提出了4种解决方案,本来是可以很哈皮的洗洗睡了,但是如果涉及到多线程环境可能就不那么乐观了。
下面的例子中开启两个子线程,一个进行遍历,另外一个有条件删除元素:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | finalList<string> myList = createTestData();newThread(newRunnable() {      @Override     publicvoidrun() {          for(String string : myList) {               System.out.println("遍历集合 value = "+ string);                          try{                    Thread.sleep(100);               } catch(InterruptedException e) {                    e.printStackTrace();               }          }     }}).start();newThread(newRunnable() {      @Override     publicvoidrun() {             for(Iterator<string> it = myList.iterator(); it.hasNext();) {           String value = it.next();                  System.out.println("删除元素 value = "+ value);                  if(value.equals( "3")) {                it.remove();           }                  try{                    Thread.sleep(100);               } catch(InterruptedException e) {                    e.printStackTrace();               }      }     }}).start();</string></string> | 
输出结果: 
遍历集合 value = 1
删除元素 value = 1
遍历集合 value = 2
删除元素 value = 2
遍历集合 value = 3
删除元素 value = 3
Exception in thread "Thread-0" 删除元素 value = 4
java.util.ConcurrentModificationException
at java.util.AbstractList$Itr.checkForComodification(Unknown Source)
at java.util.AbstractList$Itr.next(Unknown Source)
at list.ConcurrentModificationExceptionStudy$1.run(ConcurrentModificationExceptionStudy.java:42)
at java.lang.Thread.run(Unknown Source)
删除元素 value = 5 
结论:
上面的例子在多线程情况下,仅使用单线程遍历中进行删除的第1种解决方案使用it.remove(),但是测试得知4种的解决办法中的1、2、3依然会出现问题。
接着来再看一下JavaDoc对java.util.ConcurrentModificationException异常的描述:
当方法检测到对象的并发修改,但不允许这种修改时,抛出此异常。
说明以上办法在同一个线程执行的时候是没问题的,但是在异步情况下依然可能出现异常。
2. 尝试方案
(1) 在所有遍历增删地方都加上synchronized或者使用Collections.synchronizedList,虽然能解决问题但是并不推荐,因为增删造成的同步锁可能会阻塞遍历操作。
(2) 推荐使用ConcurrentHashMap或者CopyOnWriteArrayList。
3. CopyOnWriteArrayList注意事项
(1) CopyOnWriteArrayList不能使用Iterator.remove()进行删除。
(2) CopyOnWriteArrayList使用Iterator且使用List.remove(Object);会出现如下异常:
java.lang.UnsupportedOperationException: Unsupported operation remove
at java.util.concurrent.CopyOnWriteArrayList$ListIteratorImpl.remove(CopyOnWriteArrayList.java:804)
4. 解决方案
单线程情况下列出4种解决方案,但是发现在多线程情况下仅有第4种方案才能在多线程情况下不出现问题。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | List<string> myList = newCopyOnWriteArrayList<string>(); myList.add( "1"); myList.add( "2"); myList.add( "3"); myList.add( "4"); myList.add( "5");newThread(newRunnable() {       @Override     publicvoidrun() {          for(String string : myList) {               System.out.println("遍历集合 value = "+ string);                           try{                    Thread.sleep(100);               } catch(InterruptedException e) {                    e.printStackTrace();               }          }     }}).start();newThread(newRunnable() {       @Override     publicvoidrun() {                 for(inti = 0; i < myList.size(); i++) {               String value = myList.get(i);                           System.out.println("删除元素 value = "+ value);                   if(value.equals( "3")) {                myList.remove(value);                i--; // 注意                                      }                   try{                    Thread.sleep(100);               } catch(InterruptedException e) {                    e.printStackTrace();               }          }     }}).start();</string></string> | 
输出结果:
删除元素 value = 1
遍历集合 value = 1
删除元素 value = 2
遍历集合 value = 2
删除元素 value = 3
遍历集合 value = 3
删除元素 value = 4
遍历集合 value = 4
删除元素 value = 5
遍历集合 value = 5
OK,搞定
 
                    
                
 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号