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

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

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

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

这个类的迭代器方法返回的迭代器是快速失败的:如果在创建迭代器之后的任何时候修改了set,​​除了通过迭代器自己的remove方法之外,迭代器抛出ConcurrentModificationException。 因此,在并发修改的情况下,迭代器快速而干净地失败,而不是在未来的未确定时间冒任意,非确定性行为的风险。

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

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

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

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

我相信对Javadoc的引用在下一步应该做的事情上是自我解释的。

另外,在你的情况下,我不明白为什么你没有使用ImmutableSet ,而不是在terms对象上创建一个HashSet(可能在临时中修改;我看不到getTerms方法的实现,但我有预测底层键集正在被修改。 创建一个不可变集将允许当前线程拥有它自己的原始键集的防御副本。

请注意,虽然可以通过使用同步集来防止ConcurrentModificationException (如Java API文档中所述),但是先决条件是所有线程都直接访问同步集合而不是后台集合(在您的情况下,这可能是不真实的) HashSet可能在一个线程中创建,而MultiMap的底层集合由其他线程修改)。 同步集合类实际上维护一个内部互斥锁,以便线程获取访问权限; 因为你不能直接从其他线程访问互斥锁(这里这样做会非常荒谬),你应该使用MultiMaps类的unmodifiableMultimap方法来查看使用keyset或MultiMap本身的防御副本(你需要从getTerms方法返回一个不可修改的MultiMap。 您还可以研究返回同步MultiMap的必要性,但同样,您需要确保任何线程都必须获取互斥锁,以保护底层集合免于并发修改。

注意,我故意忽略提到使用线程安全的HashSet ,唯一的原因是我不确定是否可以确保对实际集合的并发访问; 它很可能不会是这种情况。


编辑:在单线程场景中的Iterator.next上抛出ConcurrentModificationException

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

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

这种可能性来自于使用方法本身,结合使用Collection的迭代器,在本例中是使用语句初始化的变量: Iterator it = C.iterator();

Collection C上迭代的Iterator存储与Collection的当前状态相关的状态。 在这种特殊情况下(假设是Sun / Oracle JRE), KeyIteratorHashSet使用的HashMap类的内部内部类)用于迭代Collection 。 此Iterator一个特殊特征是它通过它的Iterator.remove方法跟踪在Collection (本例中为HashMap )上执行的结构修改的数量。

当您直接调用Collection上的remove ,然后通过调用Iterator.next进行跟踪时, Iterator.next器会抛出ConcurrentModificationException ,因为Iterator.nextvalidationIterator.next是否发生了Iterator未发现的任何结构修改。 在这种情况下, Collection.remove会导致结构修改,由Collection跟踪,但不会由Iterator跟踪。

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

 final Multimap terms = getTerms(bq); for (Term t : terms.keySet()) { Collection C = new HashSet(terms.get(t)); if (!C.isEmpty()) { for (Iterator 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. } } } 

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

这个怎么运作 :

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

因此,当您通过迭代器调用it.remove()时, it.remove() modification-number-variable的值增加1。

但是当你直接在集合上调用collection.remove()时,它只会增加集合的modification-number变量的值,而不会增加迭代器的变量值。

规则是:只要迭代器的modify-number值与原始集合的modify-number值不匹配,它就会产生ConcurrentModificationException。

Vineet Reynolds详细解释了集合抛出ConcurrentModificationException (线程安全,并发)的原因。 Swagatika详细解释了这种机制的实现细节(集合和迭代器如何保持修改次数)。

他们的回答很有趣,我对它们进行了投票。 但是,在您的情况下,问题不是来自并发(您只有一个线程),而实现细节虽然有趣,但不应该在这里考虑。

你应该只考虑HashSet javadoc的这一部分:

这个类的迭代器方法返回的迭代器是快速失败的:如果在创建迭代器之后的任何时候修改了set,​​除了通过迭代器自己的remove方法之外,迭代器抛出ConcurrentModificationException。 因此,在并发修改的情况下,迭代器快速而干净地失败,而不是在未来的未确定时间冒任意,非确定性行为的风险。

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

更换

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

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

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

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

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