并发修改异常ConcurrentModificationException详解(一)https://developer.aliyun.com/article/1393271
继续Debug,我们记录一下几次hasNext()/next()方法时,其中几个重要变量值的变化过程。
第一次调用hasNext(): cursor = 0 size = 3 第一次调用iterator.next(): 第一次调用checkForComodification():modCount = 3 expectedModCount = 3 由于 modCount = expectedModCount ,不会发生并发修改异常。并且返回当前游标对应的值,即返回1. 由于满足Integer.parseInt(nextElement) < 2,所以会执行list.add("2")方法,之前已经了解到,add()方法会 修改modCount的值 + 1· 所以此时modCount的值变为4了. 第一次next()方法调用完,cursor游标的值会加一,所以cursor = 1. =============================================================================================================== 第二次调用hasNext(): cursor = 1 size = 4 第二次调用iterator.next(): 第二次调用checkForComodification():modCount = 4 expectedModCount = 3 由于 modCount != expectedModCount ,此时会发生并发修改异常。 以上就是ConcurrentModificationException一场产生的简单解析过程。
下图是发生并发修改异常时checkForComodification()方法的执行过程,注意modCount和expectedModCount 的值:
四、并发修改异常的特殊情况
示例:已知集合中有三个元素:"chinese"、"math"、"english",使用迭代器进行遍历, 判断集合中存在"english",如果存在则删除。
public class TestConcurrentModifyException { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("chinese"); list.add("math"); list.add("english"); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String nextElement = iterator.next(); if ("english".equals(nextElement)) { //使用ArrayList的boolean remove(Object o)方法进行删除 list.remove("english"); } } } }
程序运行结果:
Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901) at java.util.ArrayList$Itr.next(ArrayList.java:851) at com.wsh.springboot.helloworld.TestConcurrentModifyException.main(TestConcurrentModifyException.java:19)
通过上面的分析,由于往集合中加入了三个元素,所以modCount实际修改次数的值为3,当我们调用iterator()获取迭代器的时候,初始化expectedModCount的值也为3。下面我们一起看一下ArrayList类中的根据元素删除方法的源码。
remove(Object o)方法源码:
public boolean remove(Object o) { //判断需要删除的元素是否为null if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { //不为null,遍历集合,使用equals进行比较是否相等 for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; } private void fastRemove(int index) { //删除元素时,实际修改次数会自增1 //此时: modCount实际修改次数为4,但是预期修改次数还是获取迭代器时候的3,两者已经不一致了。 modCount++; //计算集合需要移动元素的个数 int numMoved = size - index - 1; if (numMoved > 0) //数组拷贝 System.arraycopy(elementData, index+1, elementData, index, numMoved); //将删除元素置为null,利于垃圾回收 elementData[--size] = null; // clear to let GC do its work }
我们分析一下程序的执行过程,查看并发修改异常是怎么产生的。当我们执行到下面一行语句之后,集合的size会减1,所以此时size = 2.
list.remove("english");
那么这时候再次执行下面的判断
while (iterator.hasNext()) {
此时cursor的值是3,但是size的值是2,两者不相等,所以hasNext()方法返回true,意味着集合中还有元素,所以还会执行一次next()方法,此时执行checkForComodification()方法,判断modCount是否等于expectedModCount,(expectedModCount=3, modCount=4),两者不相等,所以这就抛出了并发修改异常。
小结论:
- 集合每次调用add方法时,实际修改次数的值modCount都会自增1;
- 在获取迭代器的时候,集合只会执行一次将实际修改集合的次数modCount的值赋值给预期修改的次数变量expectedModCount;
- 集合在删除元素的时候,也会针对实际修改次数modCount的变量进行自增操作;
下面再来看一个并发修改异常的特殊情况,观察下面的程序:
示例:已知集合中有三个元素:"chinese"、"math"、"english",使用迭代器进行遍历,判断集合中存在"math",如果存在则删除。
public class TestConcurrentModifyException { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("chinese"); list.add("math"); list.add("english"); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String nextElement = iterator.next(); if ("math".equals(nextElement)) { //使用ArrayList的boolean remove(Object o)方法进行删除 list.remove("math"); } } System.out.println(list); } }
我们可以看到,这个示例跟上面一个实例非常相似,唯一不同的是这次删除的元素是集合中的倒数第二个元素。
程序运行结果:
[chinese, english]
我们看到,这里并没有发生并发修改异常,很神奇,而且成功删除”math“这个元素,这是为什么呢?上面一个示例明明说了会发生并发修改异常。下面我们还是分析一下其中的特殊原因:
第一次调用hasNext(): cursor = 0 size = 3, hasNext()返回true 第一次调用iterator.next(): 第一次调用checkForComodification():modCount = 3 expectedModCount = 3 由于 modCount = expectedModCount ,不会发生并发修改异常。 第一次next()方法调用完,cursor游标的值会加一,所以cursor = 1. =============================================================================================================== 第二次调用hasNext(): cursor = 1 size = 3, hasNext()返回true 第二次调用iterator.next(): 第二次调用checkForComodification():modCount = 3 expectedModCount = 3 由于 modCount = expectedModCount ,不会发生并发修改异常。 第二次next()方法调用完,cursor游标的值会加一,所以cursor = 2. 由于上面的示例中,"math"元素刚好在第二个,所以这时候"math".equals(nextElement)会返回true, 所以会执行集合的删除元素方法,size会减一,实际修改次数modCount会加一,所以size = 2 modCount = 4 ps:这里注意cursor游标的值也是2,size的值也是2, =============================================================================================================== 第三次调用hasNext(): cursor = 2 size = 2, 两者相等,所以hasNext()返回false,while循环结束,意味着不会调用next()方法, 不会执行调用checkForComodification()方法,那么肯定就不会发生并发修改异常。
小结论:
- 当要删除的元素在集合中的倒数第二个元素的时候,删除元素不会产生并发修改异常。
- 原因:因为在调用hasNext()方法的时候,cursor = size是相等的,hasNext()方法会返回false, 所以不会执行next()方法,也就不会调用checkForComodification()方法,就不会发生并发修改异常。
五、如何避免并发修改异常?
如何避免并发修改异常还有它的特殊情况呢,其实Iterator迭代器里面已经提供了remove(),用于在迭代过程对集合结构进行修改,使用iterator.remove()不会产生并发修改异常,为什么迭代器的删除方法不会产生异常呢,我们得去看看Itr内部类的remove()源码:
//迭代器自带的删除方法 public void remove() { if (lastRet < 0) throw new IllegalStateException(); //校验是否产生并发修改异常 checkForComodification(); try { //真正删除元素的方法还是调用的ArrayList的删除方法 //根据索引进行删除 ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; //每次删除完成后,会重新将expectedModCount重新赋值,值就是实际修改次数modCount的值 //这就保证了,实际修改次数modCount一定会等于预期修改次数expectedModCount ,所以不会产生并发修改异常. expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } }
小结论:
- 迭代器调用remove()方法删除元素,底层还是调用的集合的删除元素的方法;
- 在调用remove()方法后,都会将modCount的值赋值给expectedModCount,保证了它两的值永远都是相等的,所以也就不会产生并发修改异常;
六、总结
以上通过几个示例讲解了并发修改异常的现象,以及分析了并发修改异常是如何产生的,在实际工作中,如果需要使用到删除集合中元素,那么我们不要使用集合自带的删除方法,我们应该使用iterator迭代器给我们提供的删除方法,这样可以很大程序避免程序发生并发修改异常ConcurrentModificationException。