【问题标题】:Java stream forEach concurrentModificationException unexpected behaviourJava 流 forEach concurrentModificationException 意外行为
【发布时间】:2018-12-01 22:19:14
【问题描述】:

当我运行以下代码时

List<Integer> list = IntStream.range(0,10).boxed().collect(Collectors.toList());
list.stream().forEach(i -> { 
    System.out.println("i:" +i);
    if (i==5) {
        System.out.println("..adding 22");
        list.add(22);
    }   
});

我得到以下输出:

i:0
i:1
i:2
i:3
i:4
i:5
..adding 22
i:6
i:7
i:8
i:9
Exception in thread "main" java.util.ConcurrentModificationException

为什么代码超过索引 5?我没想到输出中会出现以下几行:

i:6
i:7
i:8
i:9

我在这里遗漏了一些东西,可能是关于 forEach 的行为。 该文档确实声明“此操作的行为是明确的不确定性”。并继续讨论并行流。我希望并行流以任何顺序执行 forEach,但串行流肯定会执行消费者连续馈送到 forEach 吗?如果是这样,为什么 Java 允许代码超出索引 5 处生成的异常?这里有一个线程,对吗?

提前谢谢你。

编辑: 感谢您到目前为止的回答。绝对清楚我的观点是,如果我这样做:

   for(int i: list){
        System.out.println("i:" +i);
        if(i==5) {
            System.out.println("..adding 22"); 
            list.add(22);
        }
    }

我明白了:

i:0
i:1
i:2
i:3
i:4
i:5
..adding 22
Exception in thread "main" java.util.ConcurrentModificationException

但我没有在 forEach 中得到它。 因此,似乎串行流 forEach 并不类似于迭代器,无论是手动(Iterator iter = list.iterator ...)还是增强的 for 循环迭代器。这出乎我的意料。但从答案看来,这是出于“性能原因”。但还是……出乎意料。 只是为了好玩,我尝试了 1m 个元素的列表:

    List<Integer> list = IntStream.range(0,1000000).boxed().collect(Collectors.toList());
    list.stream().forEach(
            i -> { 
                if(i%250000==0)
                    System.out.println("i:" +i);
                if(i>999997)
                    System.out.println("i:" +i);

                if(i==5) {
                    System.out.println("..adding 22"); 
                    list.add(22);
            }}                        
            );

我得到了(现在预期的)以下输出:

i:0
..adding 22
i:250000
i:500000
i:750000
i:999998
i:999999
Exception in thread "main" java.util.ConcurrentModificationException

如前所述,检查似乎在最后完成。

【问题讨论】:

  • beyond index 5 是一个实现细节,下个版本可能会beyond 3,重点是list.add(22);打破了不干涉。

标签: java list lambda java-8 java-stream


【解决方案1】:

您看到的行为特定于 ArrayListSpliteratorStream 使用 ArrayList

代码中的注释解释了实现选择:

我们在forEach(对性能最敏感的方法)[JDK 8 source code]结束时只执行一次ConcurrentModificationException检查

这与并发修改检查的约定一致。在修改的情况下,迭代器不需要快速失败。如果他们确实选择快速失败,实施者可以决定实施检查的严格程度。例如,如上所述,通常需要在正确性和性能之间进行权衡。

检测到对象并发修改的方法可能抛出此异常。 [...] 快速失败操作会尽最大努力抛出ConcurrentModificationException。因此,编写一个依赖此异常的正确性的程序是错误的:ConcurrentModificationException 应仅用于检测错误。 [Java SE 8 API docs]

【讨论】:

    【解决方案2】:

    嗯,ArrayListSpliterator下面有评论:

    如果 ArrayLists 是不可变的,或者结构上不可变的(没有添加、删除等),我们可以使用 Arrays.spliterator 实现它们的拆分器。相反,我们在不牺牲太多性能的情况下,在遍历过程中检测到尽可能多的干扰

    因此,在源List 上发生干扰的时间检查并没有完成(可能,我没有对实现进行过多研究)每个元素,但在其他时候,主要目标是不牺牲性能,但在“编辑”源时仍然失败。

    在这种情况下,您仍然违反non-interference,并且您的代码仍然会在稍后而不是更早地失败。

    【讨论】:

      【解决方案3】:

      更新您的代码:使用 CopyOnWriteArrayList

          List<Integer> list  = new CopyOnWriteArrayList<>(IntStream.range(0, 10).boxed().collect(Collectors.toList()));
          list.stream().forEach(i -> {
              System.out.println("i:" + i);
              if (i == 5) {
                  System.out.println("..adding 22");
                  list.add(22);
              }
          });
      

      【讨论】:

      • 我知道 COWList 允许并发修改。那不是我的问题。
      猜你喜欢
      • 1970-01-01
      • 2017-05-19
      • 1970-01-01
      • 2018-07-08
      • 2011-08-25
      • 1970-01-01
      • 1970-01-01
      • 2011-10-06
      • 1970-01-01
      相关资源
      最近更新 更多