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

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 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移除元素 导致并发修改异常的问题进行了全面深入地剖析。

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

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

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

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

      相关文章
      |
      7天前
      |
      存储 缓存 安全
      除了变量,final还能修饰哪些Java元素
      在Java中,final关键字不仅可以修饰变量,还可以用于修饰类、方法和参数。修饰类时,该类不能被继承;修饰方法时,方法不能被重写;修饰参数时,参数在方法体内不能被修改。
      |
      11天前
      |
      监控 Java 应用服务中间件
      高级java面试---spring.factories文件的解析源码API机制
      【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
      39 2
      |
      15天前
      |
      Java
      轻松上手Java字节码编辑:IDEA插件VisualClassBytes全方位解析
      本插件VisualClassBytes可修改class字节码,包括class信息、字段信息、内部类,常量池和方法等。
      66 6
      |
      2天前
      |
      数据采集 存储 Web App开发
      Java爬虫:深入解析商品详情的利器
      在数字化时代,信息处理能力成为企业竞争的关键。本文探讨如何利用Java编写高效、准确的商品详情爬虫,涵盖爬虫技术概述、Java爬虫优势、开发步骤、法律法规遵守及数据处理分析等内容,助力电商领域市场趋势把握与决策支持。
      |
      7天前
      |
      存储 安全 Java
      Java多线程编程中的并发容器:深入解析与实战应用####
      在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
      |
      13天前
      |
      存储 算法 Java
      Java Set深度解析:为何它能成为“无重复”的代名词?
      Java的集合框架中,Set接口以其“无重复”特性著称。本文解析了Set的实现原理,包括HashSet和TreeSet的不同数据结构和算法,以及如何通过示例代码实现最佳实践。选择合适的Set实现类和正确实现自定义对象的hashCode()和equals()方法是关键。
      23 4
      |
      13天前
      |
      Java
      那些与Java Set擦肩而过的重复元素,都经历了什么?
      在Java的世界里,Set如同一位浪漫而坚定的恋人,只对独一无二的元素情有独钟。重复元素虽屡遭拒绝,但通过反思和成长,最终变得独特,赢得了Set的认可。示例代码展示了这一过程,揭示了成长与独特性的浪漫故事。
      18 4
      |
      16天前
      |
      Java 编译器 数据库连接
      Java中的异常处理机制深度解析####
      本文深入探讨了Java编程语言中异常处理机制的核心原理、类型及其最佳实践,旨在帮助开发者更好地理解和应用这一关键特性。通过实例分析,揭示了try-catch-finally结构的重要性,以及如何利用自定义异常提升代码的健壮性和可读性。文章还讨论了异常处理在大型项目中的最佳实践,为提高软件质量提供指导。 ####
      |
      18天前
      |
      存储 算法 Java
      为什么Java Set如此“挑剔”,连重复元素都容不下?
      在Java的集合框架中,Set是一个独特的接口,它严格要求元素不重复,适用于需要唯一性约束的场景。Set通过内部数据结构(如哈希表或红黑树)和算法(如哈希值和equals()方法)实现这一特性,自动过滤重复元素,简化处理逻辑。示例代码展示了Set如何自动忽略重复元素。
      24 1
      |
      9天前
      |
      Java 开发者
      Java多线程编程中的常见误区与最佳实践####
      本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####

      推荐镜像

      更多
      下一篇
      无影云桌面