java 使用带有 Maps 键集的流时出现 ConcurrentModificationException

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/32580801/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-11-02 20:26:21  来源:igfitidea点击:

ConcurrentModificationException when using stream with Maps key set

javaforeachlambdahashmapjava-stream

提问by jaskmar

I want to remove all items from someMapwhich keys are not present in someList. Take a look into my code:

我想删除someMap.key 中不存在键的所有项目someList。看看我的代码:

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

I receive java.util.ConcurrentModificationException. Why? Stream is not parallel. What is the most elegant way to do this?

我收到java.util.ConcurrentModificationException。为什么?流不是并行的。什么是最优雅的方式来做到这一点?

回答by Tagir Valeev

@Eran already explainedhow to solve this problem better. I will explain why ConcurrentModificationExceptionoccurs.

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

The ConcurrentModificationExceptionoccurs because you are modifying the stream source. Your Mapis likely to be HashMapor TreeMapor other non-concurrent map. Let's assume it's a HashMap. Every stream is backed by Spliterator. If spliterator has no IMMUTABLEand CONCURRENTcharacteristics, then, as documentation says:

ConcurrentModificationException发生这种情况是因为您正在修改流源。您Map很可能是HashMapTreeMap或 其他非并发映射。让我们假设它是一个HashMap. 每个流都由Spliterator. 如果 spliterator 没有IMMUTABLECONCURRENT特性,那么,正如文档所说:

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

绑定 Spliterator 后,ConcurrentModificationException如果检测到结构干扰,应尽最大努力抛出。这样做的 Spliterator 被称为fail-fast

So the HashMap.keySet().spliterator()is not IMMUTABLE(because this Setcan be modified) and not CONCURRENT(concurrent updates are unsafe for HashMap). So it just detects the concurrent changes and throws a ConcurrentModificationExceptionas spliterator documentation prescribes.

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

Also it worth citing the HashMapdocumentation:

还值得引用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 ConcurrentModificationExceptionon 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.

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

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

While it says about iterators only, I believe it's the same for spliterators.

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

回答by Eran

You don't need the StreamAPI for that. Use retainAllon the keySet. Any changes on the Setreturned by keySet()are reflected in the original Map.

您不需要StreamAPI。使用retainAllkeySet。对Set返回的任何更改keySet()都反映在原始Map.

someMap.keySet().retainAll(someList);

回答by thecoop

Your stream call is (logically) doing the same as:

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

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

If you run this, you will find it throws ConcurrentModificationException, because it is modifying the map at the same time as you're iterating over it. If you have a look at the docs, you'll notice the following:

如果你运行它,你会发现它 throws 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.

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

This is what you are doing, the map implementation you're using evidently has fail-fast iterators, therefore this exception is being thrown.

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

One possible alternative is to remove the items using the iterator directly:

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

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

回答by Ron McLeod

Later answer, but you could insert a collector into your pipeline so that forEach is operating on a Set which holds a copy of the keys:

稍后回答,但您可以在管道中插入一个收集器,以便 forEach 对包含键副本的 Set 进行操作:

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