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

sim*_*ico 10 java collections multimap guava concurrentmodification

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);
                }
            }
        }
Run Code Online (Sandbox Code Playgroud)

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

Vin*_*lds 24

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

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

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

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

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

Set s = Collections.synchronizedSet(new HashSet(...));
Run Code Online (Sandbox Code Playgroud)

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

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

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

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


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

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

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

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

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

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

要克服这部分问题,你必须调用Iterator.remove而不是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.
                }
            }
        }
Run Code Online (Sandbox Code Playgroud)


Swa*_*ika 8

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

这个怎么运作 :

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

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

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

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