为什么 it.next() 抛出 java.util.ConcurrentModificationException?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/7237886/
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-10-30 19:14:10  来源:igfitidea点击:

Why does it.next() throw java.util.ConcurrentModificationException?

javacollectionsguavamultimapconcurrentmodification

提问by simpatico

final Multimap<Term, BooleanClause> terms = getTerms(bq);
        for (Term t : terms.keySet()) {
            Collection<BooleanClause> C = new HashSet(terms.get(t));
            if (!C.isEmpty()) {
                for (Iterator<BooleanClause> it = C.iterator(); it.hasNext();) {
                    BooleanClause c = it.next();
                    if(c.isSomething()) C.remove(c);
                }
            }
        }

Not a SSCCE, but can you pick up the smell?

不是 SSCCE,但你能闻到气味吗?

回答by Vineet Reynolds

The Iteratorfor the HashSetclass is a fail-fast iterator. From the documentation of the HashSetclass:

IteratorHashSet类是快速失败的迭代器。从HashSet类的文档:

The iterators returned by this class's iterator method are fail-fast: if the set is modified at any time after the iterator is created, in any way except through the iterator's own remove method, the Iterator throws 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.

此类的迭代器方法返回的迭代器是快速失败的:如果在迭代器创建后的任何时间以任何方式修改了集合,除了通过迭代器自己的 remove 方法,迭代器将抛出 ConcurrentModificationException。因此,面对并发修改,迭代器快速而干净地失败,而不是冒着在未来不确定的时间出现任意、非确定性行为的风险。

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

Note the last sentence - the fact that you are catching a ConcurrentModificationExceptionimplies that another thread is modifying the collection. The same Javadoc API page also states:

请注意最后一句话 - 您捕获 a 的事实ConcurrentModificationException意味着另一个线程正在修改集合。同一个 Javadoc API 页面还指出:

If multiple threads access a hash set concurrently, and at least one of the threads modifies the set, it must be synchronized externally. This is typically accomplished by synchronizing on some object that naturally encapsulates the set. If no such object exists, the set should be "wrapped" using the Collections.synchronizedSetmethod. This is best done at creation time, to prevent accidental unsynchronized access to the set:

Set s = Collections.synchronizedSet(new HashSet(...));

如果多个线程同时访问一个散列集,并且至少有一个线程修改了该集,则必须在外部进行同步。这通常是通过同步一些自然封装集合的对象来完成的。如果不存在这样的对象,则应使用Collections.synchronizedSet方法“包装”该集合。这最好在创建时完成,以防止对集合的意外不同步访问:

Set s = Collections.synchronizedSet(new HashSet(...));

I believe the references to the Javadoc are self explanatory in what ought to be done next.

我相信对 Javadoc 的引用对于接下来应该做什么是不言自明的。

Additionally, in your case, I do not see why you are not using the ImmutableSet, instead of creating a HashSet on the termsobject (which could possibly be modified in the interim; I cannot see the implementation of the getTermsmethod, but I have a hunch that the underlying keyset is being modified). Creating a immutable set will allow the current thread to have it's own defensive copy of the original key-set.

此外,在您的情况下,我不明白您为什么不使用ImmutableSet, 而不是在terms对象上创建一个 HashSet (可能会在此期间进行修改;我看不到该getTerms方法的实现,但我有一种预感正在修改底层密钥集)。创建一个不可变集将允许当前线程拥有它自己的原始键集的防御副本。

Note, that although a ConcurrentModificationExceptioncan be prevented by using a synchronized Set (as noted in the Java API documentation), it is a prerequisite that all threads access the synchronized collection and not the backing collection directly (which might be untrue in your case as the HashSetis probably created in one thread, while the underlying collection for the MultiMapis modified by other threads). The synchronized collection classes actually maintain an internal mutex for threads to acquire access to; since you cannot access the mutex directly from other threads (and it would be quite ridiculous to do so here), you ought to look at using a defensive copy of either the keyset or of the MultiMap itself using the unmodifiableMultimapmethod of the MultiMapsclass(you'll need to return an unmodifiable MultiMap from the getTerms method). You could also investigate the necessity of returning a synchronized MultiMap, but then again, you'll need to ensure that the mutex must be acquired by any thread to protect the underlying collection from concurrent modifications.

请注意,尽管ConcurrentModificationException可以通过使用同步 Set(如 Java API 文档中所述)来防止 a,但前提是所有线程都访问同步集合而不是直接访问支持集合(这在您的情况下可能不正确,因为HashSet可能在一个线程中创建,而 的底层集合MultiMap由其他线程修改)。同步集合类实际上维护了一个内部互斥锁,供线程访问;由于您无法直接从其他线程访问互斥锁(在这里这样做会很荒谬),您应该使用方法查看使用密钥集或 MultiMap 本身的防御性副本unmodifiableMultimapMultiMaps(您需要从 getTerms 方法返回一个不可修改的 MultiMap)。您还可以调查返回同步 MultiMap的必要性,但话又说回来,您需要确保任何线程都必须获取互斥锁,以保护底层集合免受并发修改。

Note, I have deliberately omitted mentioning the use of a thread-safe HashSetfor the sole reason that I'm unsure of whether concurrent access to the actual collection will be ensured; it most likely will not be the case.

请注意,我故意省略了使用线程安全HashSet的唯一原因,即我不确定是否可以确保对实际集合的并发访问;情况很可能并非如此。



Edit: ConcurrentModificationExceptions thrown on Iterator.nextin a single-threaded scenario

编辑:ConcurrentModificationExceptionsIterator.next在单线程场景中抛出

This is with respect to the statement: if(c.isSomething()) C.remove(c);that was introduced in the edited question.

这是关于声明的:if(c.isSomething()) C.remove(c);这是在编辑过的问题中引入的。

Invoking Collection.removechanges the nature of the question, for it now becomes possible to have ConcurrentModificationExceptions thrown even in a single-threaded scenario.

调用Collection.remove改变了问题的性质,因为现在ConcurrentModificationException即使在单线程场景中也可以抛出 s。

The possibility arises out of the use of the method itself, in conjunction with the use of the Collection's iterator, in this case the variable itthat was initialized using the statement : Iterator<BooleanClause> it = C.iterator();.

可能性源于方法本身的使用,结合Collection的迭代器的使用,在这种情况下it是使用语句初始化的变量: Iterator<BooleanClause> it = C.iterator();

The Iteratoritthat iterates over CollectionCstores state pertinent to the current state of the Collection. In this particular case (assuming a Sun/Oracle JRE), a KeyIterator(an internal inner class of the HashMapclass that is used by the HashSet) is used to iterate through the Collection. A particular characteristic of this Iteratoris that it tracks the number of structural modifications performed on the Collection(the HashMapin this case) via it's Iterator.removemethod.

Iteratorit该迭代CollectionC门店状态相关的的当前状态Collection。在这种特定情况下(假设在Sun / Oracle的JRE),一KeyIterator(内内部类的HashMap所使用的类HashSet)通过用于迭代Collection。它的一个特殊特征Iterator是它通过它的方法跟踪对CollectionHashMap在这种情况下)执行的结构修改的数量Iterator.remove

When you invoke removeon the Collectiondirectly, and then follow it up with an invocation of Iterator.next, the iterator throws a ConcurrentModificationException, as Iterator.nextverifies whether any structural modifications of the Collectionhave occurred that the Iteratoris unaware of. In this case, Collection.removecauses a structural modification, that is tracked by the Collection, but not by the Iterator.

当你调用removeCollection直接,然后用调用跟进Iterator.next,迭代器抛出一个ConcurrentModificationException,作为Iterator.next验证的任何结构的改变是否Collection已经发生的Iterator是不知道的。在这种情况下,Collection.remove会导致结构修改,即由 跟踪Collection,但不由跟踪Iterator

To overcome this part of the problem, you must invoke Iterator.removeand not Collection.remove, for this ensures that the Iteratoris now aware of the modification to the Collection. The Iteratorin this case, will track the structural modification occurring through the removemethod. Your code should therefore look like the following:

要解决这部分问题,您必须调用Iterator.remove和 not Collection.remove,因为这确保了Iterator现在知道对Collection. 在Iterator这种情况下,将跟踪通过发生的结构修饰remove方法。因此,您的代码应如下所示:

final Multimap<Term, BooleanClause> terms = getTerms(bq);
        for (Term t : terms.keySet()) {
            Collection<BooleanClause> C = new HashSet(terms.get(t));
            if (!C.isEmpty()) {
                for (Iterator<BooleanClause> it = C.iterator(); it.hasNext();) {
                    BooleanClause c = it.next();
                    if(c.isSomething()) it.remove(); // <-- invoke remove on the Iterator. Removes the element returned by it.next.
                }
            }
        }

回答by Swagatika

The reason is that you are trying to modify the collection outside iterator.

原因是您试图在迭代器之外修改集合。

How it works :

怎么运行的 :

When you create an iterator the collection maintains a modificationNum-variable for both the collection and the iterator independently. 1. The variable for collection is being incremented for each change made to the collection and and iterator.2. The variable for iterator is being incremented for each change made to the iterator.

当您创建迭代器时,集合会独立地为集合和迭代器维护一个 modifyNum 变量。1. 对于集合和迭代器的每次更改,集合的变量都会递增2. 迭代器的变量随着迭代器的每次更改而递增

So when you call it.remove()through iterator that increases the value of both the modification-number-variable by 1.

因此,当您it.remove()通过迭代器调用时,将修改编号变量的值都增加 1。

But again when you call collection.remove()on collection directly, that increments only the value of the modification-numbervariable for the collection, but not the variable for the iterator.

但同样,当您collection.remove()直接调用集合时,只会增加集合的修改编号变量的值,而不是迭代器的变量。

And rule is : whenever the modification-number value for the iterator does not match with the original collection modification-number value, it gives ConcurrentModificationException.

规则是:每当迭代器的修改编号值与原始集合修改编号值不匹配时,它给出 ConcurrentModificationException。

回答by Etienne Neveu

Vineet Reynolds has explained in great details the reasons why collections throw a ConcurrentModificationException(thread-safety, concurrency). Swagatika has explained in great details the implementation details of this mechanism (how collection and iterator keep count of the number of modifications).

Vineet Reynolds 非常详细地解释了集合抛出ConcurrentModificationException(线程安全、并发)的原因。Swagatika 非常详细地解释了这个机制的实现细节(收集和迭代器如何计算修改次数)。

Their answers were interesting, and I upvoted them. But, in your case, the problem does not come from concurrency (you have only one thread), and implementation details, while interesting, should not be considered here.

他们的回答很有趣,我赞成他们。但是,就您而言,问题并非来自并发性(您只有一个线程),并且实现细节虽然有趣,但不应在此处考虑。

You should only consider this part of the HashSetjavadoc:

您应该只考虑HashSetjavadoc 的这一部分:

The iterators returned by this class's iterator method are fail-fast: if the set is modified at any time after the iterator is created, in any way except through the iterator's own remove method, the Iterator throws 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.

此类的迭代器方法返回的迭代器是快速失败的:如果在迭代器创建后的任何时间以任何方式修改了集合,除了通过迭代器自己的 remove 方法,迭代器将抛出 ConcurrentModificationException。因此,面对并发修改,迭代器快速而干净地失败,而不是冒着在未来不确定的时间出现任意、非确定性行为的风险。

In your code, you iterate over your HashSet using its iterator, but you use the HashSet's own remove method to remove elements ( C.remove(c)), which causes the ConcurrentModificationException. Instead, as explained in the javadoc, you should use the Iterator's own remove()method, which removes the element being currently iterated from the underlying collection.

在您的代码中,您使用其迭代器迭代您的 HashSet,但您使用 HashSet 自己的 remove 方法来删​​除元素 ( C.remove(c)),这会导致ConcurrentModificationException. 相反,如 javadoc 中所述,您应该使用Iterator的自己的remove()方法,该方法从基础集合中删除当前正在迭代的元素。

Replace

代替

                if(c.isSomething()) C.remove(c);

with

                if(c.isSomething()) it.remove();

If you want to use a more functional approach, you could create a Predicateand use Guava's Iterables.removeIf()method on the HashSet:

如果您想使用更实用的方法,您可以创建一个Predicate并在 上使用 Guava 的Iterables.removeIf()方法HashSet

Predicate<BooleanClause> ignoredBooleanClausePredicate = ...;
Multimap<Term, BooleanClause> terms = getTerms(bq);
for (Term term : terms.keySet()) {
    Collection<BooleanClause> booleanClauses = Sets.newHashSet(terms.get(term));
    Iterables.removeIf(booleanClauses, ignoredBooleanClausePredicate);
}

PS: note that in both cases, this will only remove elements from the temporary HashSet. The Multimapwon't be modified.

PS:请注意,在这两种情况下,这只会从临时HashSet. 该Multimap不会被修改。