继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

Java foreach 中List移除元素抛出ConcurrentModificationException原因全解析

明明如月
关注TA
已关注
手记 59
粉丝 3857
获赞 1465

一、背景

本文重点探讨 foreach 循环中List 移除元素造成 java.util.ConcurrentModificationException 异常的原因。


先看《阿里巴巴 Java开发手册》中的相关规定:

http://img2.mukewang.com/5dd10185000179dd23561622.jpg


那么思考几个问题:

  • 反例的运行结果怎样?

  • 造成这种现象的根本原因是什么?

  • 有没有更优雅地的移除元素姿势?

本文将为你深度解读该问题。

二、解读

2.0 反例源代码


public class ListExceptionDemo {    
   public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");        
        for (String item : list) {            
        if ("1".equals(item)) {
                list.remove(item);
            }
        }
    }
}


2.1 反例的运行结果

当 if 的判断条件是 “1”.equals(item) 时,程序没有抛出任何异常。

 if ("1".equals(item)) {
        list.remove(item);
 }


而当判断条件是 :"2".equals(item)时,运行会报 java.util.ConcurrentModificationException。


2.2 原因分析

2.2.1 错误提示

既然报错,那么好办,直接看错误提示呗。

Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
    at java.util.ArrayList$Itr.next(ArrayList.java:859)
    at com.chujianyun.common.collection.list.ListExceptionDemo.main(ListExceptionDemo.java:13)

啥 ConcurrentModificationException? 并发修改异常? 一个线程哪来的并发呢?

对应的时序图

http://img.mukewang.com/5dd103a70001dbd006030310.jpg

然后我们通过错误提示看源码:我们看到错误的原因是执行 ArrayList的 Itr.next 取下一个元素检查 并发修改是

 public E next() {
      checkForComodification();      
      int i = cursor;      
      if (i >= size)            
      throw new NoSuchElementException();
       Object[] elementData = ArrayList.this.elementData;       
       if (i >= elementData.length)               
          throw new ConcurrentModificationException();
       cursor = i + 1;       
       return (E) elementData[lastRet = i];
 }


modCount 和 expectedModCount不一致导致的:

 final void checkForComodification() 
 { 
    if (modCount != expectedModCount)        
       throw new ConcurrentModificationException();
 }


因此可以推测出发生异常的根本原因在于:取下一个元素时,检查 modCount,发现不一致


2.2.2 代码调试法

为了验证上面的推测,大家可以在上述两个关键函数上打断点,通过单步了解程序的运行步骤。

我们通过调试可以“观察到”,ArrayList中的 foreach 循环的语法糖最终迭代器Array$Itr 实现的。

通过断点我们发现,ArrayList 构造内部类 Itr 对象时 expectedModCount 的值为 ArrayList的 modCount。 

运行 next 函数时会检查List 中的 modCount 的值 和 构造迭代器时“备份的” expectedModCount 是否相等。

http://img4.mukewang.com/5dd1021e0001ea8e11570690.jpg

通过调试我们还发现:虽然原始 list 至于两个元素,for each 循环执行两次后,满足if 条件移除 值为“2”的元素之后, foreach 循环依然可以进入,此时会再次通过 next 取出 list中的元素,又会执行  checkForComodification函数检查上述两个值是否相等,此时不等,抛出异常。

http://img2.mukewang.com/5dd1022e0001fb1407970163.jpg


那么这里有存在两个问题:

  1. 为什么 List 为 2  , next 却执行了 3 次呢?

  2. 如果不通过调试我们怎么知道 foreach 语法糖的底层如何实现的呢?

带着这两个问题,我们继续深入研究下去。


2.2.3  源码解析

我们查看  ArrayList$Itr 的 hasNext 函数:

 private class Itr implements Iterator<E> {        
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        Itr(){}    
            
        public boolean hasNext() {            
          return cursor != size;
        }
       // 其他省略
}


发现ArrayList的迭代器判断是否有下一个元素的标准是将下一个待返回的元素的索引和 size 比,不等表示还有下一个元素。

我们重新看源码:

 public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");        
        
        for (String item : list) {            
           if ("2".equals(item)) {
                list.remove(item);
           }
        }
    }


最初 List 中有两个元素,expectedModCount  值为2。

遍历第一个时没有走到if, 遍历第二个元素时走到if ,通过 List.remove 函数移除了元素。

   public boolean remove(Object o) {        
   if (o == null) {            
   for (int index = 0; index < size; index++)                
      if (elementData[index] == null) {
            fastRemove(index);                   
            return true;
        }
        } else {            
        for (int index = 0; index < size; index++)                
             if (o.equals(elementData[index])) {
                    fastRemove(index);                    
                    return true;
                }
        }        
        return false;
    }


而remove会调用 fastRemove 函数实际移除掉元素,在此函数中会将 modCount+1,即 modCount的值为3。

 private void fastRemove(int index) {
        modCount++;        
        int numMoved = size - index - 1;        
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }


因此在次进入foreach 时,expectedModCount  值 和 modCount的值 不相等,因此认为还有下一个元素。

但是调用迭代器的 next 函数时需检查两者是相等,发现不等,抛出ConcurrentModificationException异常。


当 if条件是  “1”.equals(item)时

 public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");        
        for (String item : list) {            
        if ("1".equals(item)) {
                list.remove(item);
            }
        }
    }


循环取出第一个元素后直接通过list给移除掉了,再次进入 foreach循环时,通过 hashNext 判断是否有下一个元素时,由于 游标==1(此时list的 size),因此判断没下一个元素。

也就是说此时循环只执行了一次就结束了,没有走到可以抛出ConcurrentModificationException异常的任何函数中,从而没有任何错误。

读到这里对迭代器的理解是不是又深了一层呢?

看到这里可能还有些同学对 foreach 究竟底层怎么实现的仍然一知半解,那么请看下一部分。


2.2.4 反汇编

话不多说,直接反汇编:

public class com.chujianyun.common.collection.list.ListExceptionDemo {
  public com.chujianyun.common.collection.list.ListExceptionDemo();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #4                  // String 1
      11: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      16: pop
      17: aload_1
      18: ldc           #6                  // String 2
      20: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      25: pop
      26: aload_1
      27: invokeinterface #7,  1            // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
      32: astore_2
      33: aload_2
      34: invokeinterface #8,  1            // InterfaceMethod java/util/Iterator.hasNext:()Z
      39: ifeq          72
      42: aload_2
      43: invokeinterface #9,  1            // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
      48: checkcast     #10                 // class java/lang/String
      51: astore_3
      52: ldc           #6                  // String 2
      54: aload_3
      55: invokevirtual #11                 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      58: ifeq          69
      61: aload_1
      62: aload_3
      63: invokeinterface #12,  2           // InterfaceMethod java/util/List.remove:(Ljava/lang/Object;)Z
      68: pop
      69: goto          33
      72: return
}


代码偏移从 0 到 25 行实现下面这部分功能:

     List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");


从 26行开始我们发现底层使用迭代器实现,我们脑补后翻译回 Java代码大致如下:

 public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");

        Iterator<String> iterator = list.iterator();        
        while (iterator.hasNext()) {
            String item = iterator.next();            
            if ("2".equals(item)) {                
               //iterator.remove();
                list.remove(item);
            }
        }
    }


大家运行“翻译”后的代码发信啊和原始代码的报错内容完全一致:

Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
    at java.util.ArrayList$Itr.next(ArrayList.java:859)
    at com.chujianyun.common.collection.list.ListException.main(ListException.java:16)


2.2.5 继续深挖

1、为啥通过 iterator.remove() 移除元素就没事呢?

我们看 java.util.ArrayList.Itr#remove 的源码:

 public void remove() {     
    if (lastRet < 0)           
       throw new IllegalStateException();
     checkForComodification();     
     try {
           ArrayList.this.remove(lastRet);
           cursor = lastRet;
           lastRet = -1;
           expectedModCount = modCount;
     } catch (IndexOutOfBoundsException ex) {       
          throw new ConcurrentModificationException();
     }
}


从这里我们看到,通过迭代器移除元素后, expectedModCount 会重新赋值为 modCount。

因此使用iterator.remove() 移除元素不报错的原因就找到了。

2、有没有比手册给出的代码更优雅的写法?

我们打开其函数列表,观察List 和其父类有没有便捷地移除元素方式:



“惊奇”地发现,Collection 接口提供了 removeIf 函数可以满足此需求。

还等啥呢,替换下,发现代码如此简洁:

 public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");        // 一行代码实现
        list.removeIf("2"::equals);
    }


自此是不是文章就该结束了呢? 

NO..  

removeIf 为啥能够实现移除元素的功能呢?

我们猜测,底层应该是遍历然后对比元素然后移除,可能也是迭代器方式,我们看源码:

java.util.Collection#removeIf

default boolean removeIf(Predicate<? super E> filter) {
        Objects.requireNonNull(filter);        
        boolean removed = false;        
        final Iterator<E> each = iterator();        
        while (each.hasNext()) {            
        if (filter.test(each.next())) {
                each.remove();
                removed = true;
            }
        }        
        return removed;
    }


我们发现和我们想的比较一致。


三、总结

本小节对《阿里巴巴 Java开发手册》中 foreach 循环 List 移除元素导致并发修改异常的问题,进行了全面深入地剖析。

希望可以帮助大家,彻底搞懂这个问题。

另外也提供了研究类似问题的一般思路,即代码调试、读源码、反汇编等。

通过这个问题,希望大家遇到问题时,能够养成深挖的精神,通过问题带动知识的理解,知其所以然。

最后提醒大家,不要看书记结论,容易忘,记住不会用,要多思考原因,才能理解更深刻。

“尽信书不如无书”,不要认为作者写的都是对的,都是最好的,要有自己的思考。


想了解更多《手册》详解的更多内容,想学习更多开发和避坑技巧等,请关注《阿里巴巴Java 开发手册》详解专栏




打开App,阅读手记
3人推荐
发表评论
随时随地看视频慕课网APP

热门评论

文章写的真是详尽!看得出明月学长喜欢追根究底。

解决问题的思路get 到了,大胆猜测小心论证~

之前就遇到过这个坑,不过只是记住了怎么做。

这篇文章讲得好透彻,没想到还可以用 removeIf 来移除元素。?

先关注一波,希望后面还可以分享更多高质量的文章。?

查看全部评论