环境:JDK 1.8.0_111
在Java开发过程中,使用iterator遍历集合的同时对集合进行修改就会出现java.util.ConcurrentModificationException异常,本文就以ArrayList为例去理解和解决这种异常。
一、单线程情况下问题分析及解决方案
1.1 问题复现
先上一段抛异常的代码。
1 public void test1() { 2 ArrayList<Integer> arrayList = new ArrayList<>(); 3 for (int i = 0; i < 20; i++) { 4 arrayList.add(Integer.valueOf(i)); 5 } 6 7 // 复现方法一 8 Iterator<Integer> iterator = arrayList.iterator(); 9 while (iterator.hasNext()) { 10 Integer integer = iterator.next(); 11 if (integer.intValue() == 5) { 12 arrayList.remove(integer); 13 } 14 } 15 16 // 复现方法二 17 iterator = arrayList.iterator(); 18 for (Integer value : arrayList) { 19 Integer integer = iterator.next(); 20 if (integer.intValue() == 5) { 21 arrayList.remove(integer); 22 } 23 } 24 }
在这个代码中展示了两种能抛异常的实现方式。
1.2、问题原因分析
先来看实现方法一,方法一中使用Iterator遍历ArrayList, 抛出异常的是iterator.next()。看下Iterator next方法实现源码
1 public E next() { 2 checkForComodification(); 3 int i = cursor; 4 if (i >= size) 5 throw new NoSuchElementException(); 6 Object[] elementData = ArrayList.this.elementData; 7 if (i >= elementData.length) 8 throw new ConcurrentModificationException(); 9 cursor = i + 1; 10 return (E) elementData[lastRet = i]; 11 } 12 13 final void checkForComodification() { 14 if (modCount != expectedModCount) 15 throw new ConcurrentModificationException(); 16 }
在next方法中首先调用了checkForComodification方法,该方法会判断modCount是否等于expectedModCount,不等于就会抛出java.util.ConcurrentModificationExcepiton异常。
我们接下来跟踪看一下modCount和expectedModCount的赋值和修改。
modCount是ArrayList的一个属性,继承自抽象类AbstractList,用于表示ArrayList对象被修改次数。
1 protected transient int modCount = 0;
整个ArrayList中修改modCount的方法比较多,有add、remove、clear、ensureCapacityInternal等,凡是设计到ArrayList对象修改的都会自增modCount属性。
在创建Iterator的时候会将modCount赋值给expectedModCount,在遍历ArrayList过程中,没有其他地方可以设置expectedModCount了,因此遍历过程中expectedModCount会一直保持初始值20(调用add方法添加了20个元素,修改了20次)。
1 int expectedModCount = modCount; // 创建对象时初始化
遍历的时候是不会触发modCount自增的,但是遍历到integer.intValue() == 5的时候,执行了一次arrayList.remove(integer),这行代码执行后modCount++变为了21,但此时的expectedModCount仍然为20。
1 final void checkForComodification() { 2 if (modCount != expectedModCount) 3 throw new ConcurrentModificationException(); 4 }
在执行next方法时,遇到modCount != expectedModCount方法,导致抛出异常java.util.ConcurrentModificationException。
明白了抛出异常的过程,但是为什么要这么做呢?很明显这么做是为了阻止程序员在不允许修改的时候修改对象,起到保护作用,避免出现未知异常。引用网上的一段解释,点击查看解释来源
Iterator 是工作在一个独立的线程中,并且拥有一个 mutex 锁。 Iterator 被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变。 当索引指针往后移动的时候就找不到要迭代的对象,所以按照 fail-fast 原则 Iterator 会马上抛出 java.util.ConcurrentModificationException 异常。 所以 Iterator 在工作的时候是不允许被迭代的对象被改变的。但你可以使用 Iterator 本身的方法 remove() 来删除对象, Iterator.remove() 方法会在删除当前迭代对象的同时维护索引的一致性。
再来分析下第二种for循环抛异常的原因:
1 public void forEach(Consumer<? super E> action) { 2 Objects.requireNonNull(action); 3 final int expectedModCount = modCount; 4 @SuppressWarnings("unchecked") 5 final E[] elementData = (E[]) this.elementData; 6 final int size = this.size; 7 for (int i=0; modCount == expectedModCount && i < size; i++) { 8 action.accept(elementData[i]); 9 } 10 if (modCount != expectedModCount) { 11 throw new ConcurrentModificationException(); 12 } 13 }
在for循环中一开始也是对expectedModCount采用modCount进行赋值。在进行for循环时每次都会有判定条件modCount == expectedModCount,当执行完arrayList.remove(integer)之后,该判定条件返回false退出循环,然后执行if语句,结果同样抛出java.util.ConcurrentModificationException异常。
这两种复现方法实际上都是同一个原因导致的。
1.3 问题解决方案
上述的两种复现方法都是在单线程运行的,先来说明单线程中的解决方案:
1 public void test2() { 2 ArrayList<Integer> arrayList = new ArrayList<>(); 3 for (int i = 0; i < 20; i++) { 4 arrayList.add(Integer.valueOf(i)); 5 } 6 7 Iterator<Integer> iterator = arrayList.iterator(); 8 while (iterator.hasNext()) { 9 Integer integer = iterator.next(); 10 if (integer.intValue() == 5) { 11 iterator.remove(); 12 } 13 } 14 }
这种解决方案最核心的就是调用iterator.remove()方法。我们看看该方法源码为什么这个方法能避免抛出异常
1 public void remove() { 2 if (lastRet < 0) 3 throw new IllegalStateException(); 4 checkForComodification(); 5 6 try { 7 ArrayList.this.remove(lastRet); 8 cursor = lastRet; 9 lastRet = -1; 10 expectedModCount = modCount; 11 } catch (IndexOutOfBoundsException ex) { 12 throw new ConcurrentModificationException(); 13 } 14 }
在iterator.remove()方法中,同样调用了ArrayList自身的remove方法,但是调用完之后并非就return了,而是expectedModCount = modCount重置了expectedModCount值,使二者的值继续保持相等。
针对forEach循环并没有修复方案,因此在遍历过程中同时需要修改ArrayList对象,则需要采用iterator遍历。
上面提出的解决方案调用的是iterator.remove()方法,如果不仅仅是想调用remove方法移除元素,还想增加元素,或者替换元素,是否可以呢?浏览Iterator源码可以发现这是不行的,Iterator只提供了remove方法。
但是ArrayList实现了ListIterator接口,ListIterator类继承了Iter,这些操作都是可以实现的,使用示例如下:
1 public void test3() { 2 ArrayList<Integer> arrayList = new ArrayList<>(); 3 for (int i = 0; i < 20; i++) { 4 arrayList.add(Integer.valueOf(i)); 5 } 6 7 ListIterator<Integer> iterator = arrayList.listIterator(); 8 while (iterator.hasNext()) { 9 Integer integer = iterator.next(); 10 if (integer.intValue() == 5) { 11 iterator.set(Integer.valueOf(6)); 12 iterator.remove(); 13 iterator.add(integer); 14 } 15 } 16 }
二、 多线程情况下的问题分析及解决方案
单线程问题解决了,再来看看多线程情况。
2.1 问题复现
1 public void test4() { 2 ArrayList<Integer> arrayList = new ArrayList<>(); 3 for (int i = 0; i < 20; i++) { 4 arrayList.add(Integer.valueOf(i)); 5 } 6 7 Thread thread1 = new Thread(new Runnable() { 8 @Override 9 public void run() { 10 ListIterator<Integer> iterator = arrayList.listIterator(); 11 while (iterator.hasNext()) { 12 System.out.println("thread1 " + iterator.next().intValue()); 13 try { 14 Thread.sleep(1000); 15 } catch (InterruptedException e) { 16 e.printStackTrace(); 17 } 18 } 19 } 20 }); 21 22 Thread thread2 = new Thread(new Runnable() { 23 @Override 24 public void run() { 25 ListIterator<Integer> iterator = arrayList.listIterator(); 26 while (iterator.hasNext()) { 27 System.out.println("thread2 " + iterator.next().intValue()); 28 iterator.remove(); 29 } 30 } 31 }); 32 thread1.start(); 33 thread2.start(); 34 }
在个测试代码中,开启两个线程,一个线程遍历,另外一个线程遍历加修改。程序输出结果如下
thread1 0 thread2 0 thread2 1 thread2 2 thread2 3 thread2 4 thread2 5 thread2 6 thread2 7 thread2 8 thread2 9 thread2 10 thread2 11 thread2 12 thread2 13 thread2 14 thread2 15 thread2 16 thread2 17 thread2 18 thread2 19 Exception in thread "Thread-0" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901) at java.util.ArrayList$Itr.next(ArrayList.java:851) at com.snow.ExceptionTest$1.run(ExceptionTest.java:74) at java.lang.Thread.run(Thread.java:745)