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

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

一、背景

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

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

image.png

那么思考几个问题:

    • 反例的运行结果怎样?
    • 造成这种现象的根本原因是什么?
    • 有没有更优雅地的移除元素姿势?

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

    二、解读

    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);
                }
            }
        }
    }

    image.gif

    2.1 反例的运行结果

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

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

    image.gif

    而当判断条件是 :"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? 并发修改异常? 一个线程哪来的并发呢?

    对应的时序图

    image.png

    然后我们通过错误提示看源码:我们看到错误的原因是执行 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];
     }

    image.gif

    modCount 和 expectedModCount不一致导致的:

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

    image.gif

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

    2.2.2 代码调试法

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

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

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

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


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

    image.png

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

      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;
              }
      // 其他省略
      }

      image.gif

      发现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);
                  }
              }
          }

      image.gif

      最初 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;
          }

      image.gif

      而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
          }

      image.gif

      因此在次进入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);
                  }
              }
          }

      image.gif

      循环取出第一个元素后直接通过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
      }

      image.gif

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

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

      image.gif

      从 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);
                  }
              }
          }

      image.gif

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

      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();
           }
      }

      image.gif

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

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

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

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

      image.png

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

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

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

      image.gif

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

      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;
          }

      image.gif

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

      三、总结

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

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

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

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

      最后 尽信书不如无书,不要止步于书中提到的内容,要多一些思考。

      相关文章
      |
      人工智能 Java
      Java 中数组Array和列表List的转换
      本文介绍了数组与列表之间的相互转换方法,主要包括三部分:1)使用`Collections.addAll()`方法将数组转为列表,适用于引用类型,效率较高;2)通过`new ArrayList&lt;&gt;()`构造器结合`Arrays.asList()`实现类似功能;3)利用JDK8的`Stream`流式计算,支持基本数据类型数组的转换。此外,还详细讲解了列表转数组的方法,如借助`Stream`实现不同类型数组间的转换,并附带代码示例与执行结果,帮助读者深入理解两种数据结构的互转技巧。
      1047 1
      Java 中数组Array和列表List的转换
      |
      存储 消息中间件 NoSQL
      Redis数据结构:List类型全面解析
      Redis数据结构——List类型全面解析:存储多个有序的字符串,列表中每个字符串成为元素 Eelement,最多可以存储 2^32-1 个元素。可对列表两端插入(push)和弹出(pop)、获取指定范围的元素列表等,常见命令。 底层数据结构:3.2版本之前,底层采用**压缩链表ZipList**和**双向链表LinkedList**;3.2版本之后,底层数据结构为**快速链表QuickList** 列表是一种比较灵活的数据结构,可以充当栈、队列、阻塞队列,在实际开发中有很多应用场景。
      |
      安全 Java 程序员
      深入Java集合框架:解密List的Fail-Fast与Fail-Safe机制
      本文介绍了 Java 中 List 的遍历和删除操作,重点讨论了快速失败(fail-fast)和安全失败(fail-safe)机制。通过普通 for 循环、迭代器和 foreach 循环的对比,详细解释了各种方法的优缺点及适用场景,特别是在多线程环境下的表现。最后推荐了适合高并发场景的 fail-safe 容器,如 CopyOnWriteArrayList 和 ConcurrentHashMap。
      421 5
      |
      Java 程序员 编译器
      Java|如何正确地在遍历 List 时删除元素
      从源码分析如何正确地在遍历 List 时删除元素。为什么有的写法会导致异常,而另一些不会。
      560 3
      |
      Java 程序员
      Java|List.subList 踩坑小记
      不应该仅凭印象和猜测,就开始使用一个方法,至少花一分钟认真读完它的官方注释文档。
      438 1
      |
      存储 编译器 C++
      【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
      【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
      364 2
      |
      Java
      用JAVA架建List集合为树形结构的代码方法
      这段代码定义了一个表示树形结构的 `Node` 类和一个用于构建树形结构的 `TreeController`。`Node` 类包含基本属性如 `id`、`pid`、`name` 和 `type`,以及子节点列表 `children`。`TreeController` 包含初始化节点列表并将其转换为树形结构的方法。通过过滤和分组操作实现树形结构的构建。详情可见:[代码示例链接1](http://www.zidongmutanji.com/zsjx/43551.html),[代码效果参考链接2](https://www.257342.com/sitemap/post.html)。
      386 5
      |
      Java API 开发者
      代码小妙招:用Java轻松获取List交集数据
      在Java中获取两个 `List`的交集可以通过 `retainAll`方法和Java 8引入的流操作来实现。使用 `retainAll`方法更为直接,但会修改原始 `List`的内容。而使用流则提供了不修改原始 `List`、更为灵活的处理方式。开发者可以根据具体的需求和场景,选择最适合的方法来实现。了解和掌握这些方法,能够帮助开发者在实际开发中更高效地处理集合相关的问题。
      752 1
      |
      设计模式 存储 安全
      【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
      结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
      924 140
      【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
      |
      算法 测试技术 C语言
      深入理解HTTP/2:nghttp2库源码解析及客户端实现示例
      通过解析nghttp2库的源码和实现一个简单的HTTP/2客户端示例,本文详细介绍了HTTP/2的关键特性和nghttp2的核心实现。了解这些内容可以帮助开发者更好地理解HTTP/2协议,提高Web应用的性能和用户体验。对于实际开发中的应用,可以根据需要进一步优化和扩展代码,以满足具体需求。
      1502 29