【问题标题】:ConcurrentModificationException when using stream with Maps key set使用带有 Maps 键集的流时出现 ConcurrentModificationException
【发布时间】:2015-12-11 09:34:53
【问题描述】:

我想从someMap 中删除所有在someList 中不存在的项。看看我的代码:

someMap.keySet().stream().filter(v -> !someList.contains(v)).forEach(someMap::remove);

我收到java.util.ConcurrentModificationException。为什么?流不是并行的。最优雅的方法是什么?

【问题讨论】:

    标签: java foreach lambda hashmap java-stream


    【解决方案1】:

    您不需要Stream API。在keySet 上使用retainAllkeySet() 返回的 Set 上的任何更改都会反映在原始 Map 中。

    someMap.keySet().retainAll(someList);
    

    【讨论】:

    • 好的,这是我第二个问题的好答案。但是我还是不知道为什么会出现java.util.ConcurrentModificationException
    【解决方案2】:

    您的流调用(在逻辑上)与以下内容相同:

    for (K k : someMap.keySet()) {
        if (!someList.contains(k)) {
            someMap.remove(k);
        }
    }
    

    如果你运行它,你会发现它抛出ConcurrentModificationException,因为它在你迭代它的同时修改了地图。如果您查看docs,您会注意到以下内容:

    请注意,此异常并不总是表示对象已被不同的线程同时修改。如果单个线程发出一系列违反对象约定的方法调用,则该对象可能会抛出此异常。例如,如果线程在使用快速失败迭代器迭代集合时直接修改集合,则迭代器将抛出此异常。

    这就是你正在做的事情,你正在使用的地图实现显然有快速失败的迭代器,因此抛出了这个异常。

    一种可能的替代方法是直接使用迭代器删除项目:

    for (Iterator<K> ks = someMap.keySet().iterator(); ks.hasNext(); ) {
        K next = ks.next();
        if (!someList.contains(k)) {
            ks.remove();
        }
    }
    

    【讨论】:

      【解决方案3】:

      @Eran 已经explained 如何更好地解决这个问题。我会解释为什么会出现ConcurrentModificationException

      ConcurrentModificationException 出现是因为您正在修改流源。您的Map 可能是HashMapTreeMap 或其他非并发映射。假设它是HashMap。每个流都由Spliterator 支持。如果 spliterator 没有 IMMUTABLECONCURRENT 特征,那么,如文档所述:

      绑定 Spliterator 后,如果检测到结构干扰,应尽最大努力抛出 ConcurrentModificationException。执行此操作的拆分器称为 fail-fast

      所以HashMap.keySet().spliterator() 不是IMMUTABLE(因为这个Set 可以修改)而不是CONCURRENT(并发更新对于HashMap 是不安全的)。所以它只是检测并发更改并按照拆分器文档的规定抛出ConcurrentModificationException

      还值得引用HashMap 文档:

      这个类的所有“集合视图方法”返回的迭代器都是fail-fast:如果地图在迭代器创建后的任何时间被结构修改,除了通过迭代器的自己的 remove 方法,迭代器会抛出一个ConcurrentModificationException。因此,面对并发修改,迭代器会快速而干净地失败,而不是在未来不确定的时间冒任意的、非确定性的行为。

      请注意,无法保证迭代器的快速失败行为,因为一般来说,在存在不同步的并发修改的情况下无法做出任何硬保证。快速失败的迭代器会尽最大努力抛出ConcurrentModificationException。因此,编写一个依赖此异常来确保其正确性的程序是错误的:迭代器的快速失败行为应该只用于检测错误

      虽然它只说迭代器,但我相信拆分器也是如此。

      【讨论】:

      • 我认为这是最好的答案,但您可以编辑它以添加@Eran 提到的解决方案。对于将来遇到同样问题的人来说,这将是 100% 的满足。
      • @MariuszJaskółka,Eran 的答案也在这里,其他人也可能会看到它。这是正确的,我赞成。我可以添加对他的解决方案的引用。
      【解决方案4】:

      稍后回答,但您可以将收集器插入到您的管道中,以便 forEach 在包含密钥副本的 Set 上运行:

      someMap.keySet()
          .stream()
          .filter(v -> !someList.contains(v))
          .collect(Collectors.toSet())
          .forEach(someMap::remove);
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-11-17
        • 1970-01-01
        • 1970-01-01
        • 2012-11-21
        相关资源
        最近更新 更多