HashSet
类的
Iterator
是一种快速失败的迭代器。从
HashSet
类的文档中可以看到:
该类的 iterator 方法返回的迭代器是快速失败的:如果在创建迭代器后集合被以任何方式(除了通过迭代器自身的 remove 方法)修改,则迭代器会抛出 ConcurrentModificationException 异常。因此,在并发修改的情况下,迭代器会迅速而干净地失败,而不是冒着在未来某个不确定的时间面临任意、不确定行为的风险。
请注意,由于存在非同步的并发修改,迭代器的快速失败行为不能得到保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException 异常。因此,编写依赖于此异常的程序来确保其正确性是错误的:迭代器的快速失败行为应仅用于检测错误。
请注意最后一句话——捕获 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.synchronizedSet method. This
is best done at creation time, to prevent accidental unsynchronized
access to the set:
Set s = Collections.synchronizedSet(new HashSet(...));
我认为Javadoc中的参考说明了下一步应该做什么。
此外,在您的情况下,我不明白为什么您没有使用ImmutableSet
,而是创建了一个HashSet对象(这可能会在中间被修改;我看不到getTerms
方法的实现,但我猜测底层的keyset正在被修改)。创建一个不可变集合将允许当前线程拥有原始键集的自己的防御性副本。
请注意,尽管使用同步的Set(如Java API文档中所述)可以防止ConcurrentModificationException异常,但前提是所有线程都访问同步集合而不是直接访问支持集合(在您的情况下可能是不真实的,因为HashSet可能在一个线程中创建,而MultiMap的基础集合则由其他线程修改)。同步集合类实际上维护了一个内部互斥体供线程获取访问权限;由于您无法直接从其他线程访问互斥体(在这里这样做非常荒谬),因此您应该考虑使用键集或MultiMap本身的防御性副本
使用MultiMaps类的unmodifiableMultimap方法(您需要从getTerms方法返回一个不可修改的MultiMap)。您还可以调查返回
同步MultiMap的必要性,但再次强调,您需要确保任何线程都必须获取互斥体以保护基础集合免受并发修改。
请注意,我故意省略了提及使用线程安全的HashSet
的原因是我不确定实际集合的并发访问是否得到保证;很可能不是这种情况。
编辑:ConcurrentModificationException
在单线程场景下在Iterator.next
上抛出
这是针对编辑后的问题中引入的语句:if(c.isSomething()) C.remove(c);
调用Collection.remove
会改变问题的性质,因为即使在单线程场景下,也有可能抛出ConcurrentModificationException
。
这种可能性源于方法本身的使用,以及与Collection
的迭代器一起使用,在此情况下使用了变量it
初始化的语句:Iterator<BooleanClause> it = C.iterator();
。
迭代Collection
C
的Iterator
it
存储与Collection
当前状态相关的状态。在这种特殊情况下(假设使用Sun/Oracle JRE),使用KeyIterator
(HashMap
类的内部内部类,由HashSet
使用)迭代Collection
。该Iterator
的一个特定特征是,它通过其Iterator.remove
方法跟踪对Collection
(在这种情况下为HashMap
)执行的结构修改次数。
当您直接在集合上调用
remove
,然后紧接着调用
Iterator.next
时,迭代器会抛出
ConcurrentModificationException
异常,因为
Iterator.next
会验证是否有任何结构修改的
Collection
是迭代器不知道的。在这种情况下,
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();
}
}
}