为什么 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
Why does it.next() throw java.util.ConcurrentModificationException?
提问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 Iterator
for the HashSet
class is a fail-fast iterator. From the documentation of the HashSet
class:
在Iterator
为HashSet
类是快速失败的迭代器。从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 ConcurrentModificationException
implies 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 terms
object (which could possibly be modified in the interim; I cannot see the implementation of the getTerms
method, 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 ConcurrentModificationException
can 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 HashSet
is probably created in one thread, while the underlying collection for the MultiMap
is 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 unmodifiableMultimap
method of the MultiMaps
class(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 本身的防御性副本unmodifiableMultimap
MultiMaps
(您需要从 getTerms 方法返回一个不可修改的 MultiMap)。您还可以调查返回同步 MultiMap的必要性,但话又说回来,您需要确保任何线程都必须获取互斥锁,以保护底层集合免受并发修改。
Note, I have deliberately omitted mentioning the use of a thread-safe HashSet
for 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: ConcurrentModificationException
s thrown on Iterator.next
in a single-threaded scenario
编辑:ConcurrentModificationException
sIterator.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.remove
changes the nature of the question, for it now becomes possible to have ConcurrentModificationException
s 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 it
that was initialized using the statement : Iterator<BooleanClause> it = C.iterator();
.
可能性源于方法本身的使用,结合Collection
的迭代器的使用,在这种情况下it
是使用语句初始化的变量: Iterator<BooleanClause> it = C.iterator();
。
The Iterator
it
that iterates over Collection
C
stores 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 HashMap
class that is used by the HashSet
) is used to iterate through the Collection
. A particular characteristic of this Iterator
is that it tracks the number of structural modifications performed on the Collection
(the HashMap
in this case) via it's Iterator.remove
method.
在Iterator
it
该迭代Collection
C
门店状态相关的的当前状态Collection
。在这种特定情况下(假设在Sun / Oracle的JRE),一KeyIterator
(内内部类的HashMap
所使用的类HashSet
)通过用于迭代Collection
。它的一个特殊特征Iterator
是它通过它的方法跟踪对Collection
(HashMap
在这种情况下)执行的结构修改的数量Iterator.remove
。
When you invoke remove
on the Collection
directly, and then follow it up with an invocation of Iterator.next
, the iterator throws a ConcurrentModificationException
, as Iterator.next
verifies whether any structural modifications of the Collection
have occurred that the Iterator
is unaware of. In this case, Collection.remove
causes a structural modification, that is tracked by the Collection
, but not by the Iterator
.
当你调用remove
上Collection
直接,然后用调用跟进Iterator.next
,迭代器抛出一个ConcurrentModificationException
,作为Iterator.next
验证的任何结构的改变是否Collection
已经发生的Iterator
是不知道的。在这种情况下,Collection.remove
会导致结构修改,即由 跟踪Collection
,但不由跟踪Iterator
。
To overcome this part of the problem, you must invoke Iterator.remove
and not Collection.remove
, for this ensures that the Iterator
is now aware of the modification to the Collection
. The Iterator
in this case, will track the structural modification occurring through the remove
method. 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 HashSet
javadoc:
您应该只考虑HashSet
javadoc 的这一部分:
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 Multimap
won't be modified.
PS:请注意,在这两种情况下,这只会从临时HashSet
. 该Multimap
不会被修改。