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

2023-11-23

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);
                }
            }
        }

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


The Iterator为了HashSet类是一个快速失败迭代器。从文档中HashSet class:

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

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

注意最后一句话——事实上你正在抓住一个ConcurrentModificationException意味着另一个线程正在修改集合。同一个 Javadoc API 页面还指出:

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

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

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

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

请注意,虽然ConcurrentModificationException可以通过使用同步集来防止(如 Java API 文档中所述),前提是所有线程访问同步集合而不是直接访问后备集合(在您的情况下这可能是不正确的,因为HashSet可能是在一个线程中创建的,而底层集合MultiMap被其他线程修改)。同步集合类实际上维护一个内部互斥锁,供线程获取访问权限;由于您无法直接从其他线程访问互斥体(在这里这样做是非常荒谬的),因此您应该考虑使用键集或 MultiMap 本身的防御性副本使用unmodifiableMultimap的方法MultiMaps class(您需要从 getTerms 方法返回一个不可修改的 MultiMap)。您还可以调查退回的必要性同步多图,但话又说回来,您需要确保任何线程都必须获取互斥锁,以保护底层集合免受并发修改的影响。

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


Edit: ConcurrentModificationException被扔在Iterator.next在单线程场景下

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

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

这种可能性是由于方法本身的使用以及与Collection的迭代器,在本例中为变量it这是使用以下语句初始化的:Iterator<BooleanClause> it = C.iterator();.

The Iterator it迭代Collection C存储与当前状态相关的状态Collection。在这种特殊情况下(假设有 Sun/Oracle JRE),KeyIterator(一个内部内部类HashMap使用的类HashSet) 用于迭代Collection。这个的一个特殊的特点是Iterator是它跟踪对结构进行的修改数量Collection (the HashMap在这种情况下)通过它的Iterator.remove method.

当你调用remove on the Collection直接,然后调用Iterator.next,迭代器抛出一个ConcurrentModificationException, as Iterator.next验证是否有任何结构修改Collection已经发生了Iterator是不知道的。在这种情况下,Collection.remove引起结构修改,该修改由Collection,但不是由Iterator.

要解决这部分问题,您必须调用Iterator.remove并不是Collection.remove,因为这确保了Iterator现在已经知道对Collection. The 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.
                }
            }
        }
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

为什么 it.next() 会抛出 java.util.ConcurrentModificationException? 的相关文章

随机推荐