使用带有地图键集的流时出现 ConcurrentModificationException

ConcurrentModificationException when using stream with Maps key set

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

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

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

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

someMap.keySet().retainAll(someList);

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

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

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

Note that this exception does not always indicate that an object has been concurrently modified by a different thread. If a single thread issues a sequence of method invocations that violates the contract of an object, the object may throw this exception. For example, if a thread modifies a collection directly while it is iterating over the collection with a fail-fast iterator, the iterator will throw this exception.

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

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

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

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

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

After binding a Spliterator should, on a best-effort basis, throw ConcurrentModificationException if structural interference is detected. Spliterators that do this are called fail-fast.

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

还值得引用 HashMap 文档:

The iterators returned by all of this class's "collection view methods" are fail-fast: if the map is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove method, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.

Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.

虽然它只提到了迭代器,但我相信拆分器也是一样的。

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

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