10
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ではありませんが、匂いを嗅ぐことができますか?

4

3 に答える 3

24

クラスのIteratorforHashSetは、フェイルファストイテレータです。HashSetクラスのドキュメントから:

このクラスのイテレータメソッドによって返されるイテレータはフェイルファストです。イテレータが作成された後、イテレータ自体のremoveメソッド以外の方法でセットが変更された場合、イテレータはConcurrentModificationExceptionをスローします。したがって、同時変更に直面した場合、イテレータは、将来の不確定な時点で任意の非決定的な動作のリスクを冒すのではなく、迅速かつクリーンに失敗します。

イテレータのフェイルファスト動作は保証できないことに注意してください。一般的に言えば、同期されていない同時変更が存在する場合、ハード保証を行うことは不可能です。フェイルファストイテレータは、ベストエフォートベースでConcurrentModificationExceptionをスローします。したがって、その正確性をこの例外に依存するプログラムを作成するのは誤りです。イテレータのフェイルファスト動作は、バグを検出するためにのみ使用する必要があります。

最後の文に注意してください-あなたがキャッチしているという事実はConcurrentModificationException、別のスレッドがコレクションを変更していることを意味します。同じJavadocAPIページにも次のように記載されています。

複数のスレッドが同時にハッシュセットにアクセスし、少なくとも1つのスレッドがセットを変更する場合は、外部で同期する必要があります。これは通常、セットを自然にカプセル化するオブジェクトを同期することによって実現されます。そのようなオブジェクトが存在しない場合は、Collections.synchronizedSetメソッドを使用してセットを「ラップ」する必要があります。これは、セットへの偶発的な非同期アクセスを防ぐために、作成時に行うのが最適です。

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

Javadocへの参照は、次に何をすべきかについて自明であると思います。

ImmutableSetさらに、あなたの場合、オブジェクトにHashSetを作成する代わりに、を使用していない理由がわかりません(これは、暫定的に変更される可能性があります。メソッドtermsの実装はわかりませんが、その予感があります。getTerms基になるキーセットが変更されています)。不変のセットを作成すると、現在のスレッドが元のキーセットの独自の防御コピーを持つことができます。

同期セットを使用することでaConcurrentModificationExceptionを防ぐことができますが(Java APIドキュメントに記載されているように)、すべてのスレッドがバッキングコレクションではなく、同期コレクションに直接アクセスすることが前提条件であることに注意してください(これは、HashSetおそらく1つのスレッドで作成されますが、の基になるコレクションはMultiMap他のスレッドによって変更されます)。同期されたコレクションクラスは、実際には、スレッドがアクセスを取得するための内部ミューテックスを維持します。他のスレッドからミューテックスに直接アクセスすることはできないため(ここでアクセスするのは非常にばかげています)、クラスのメソッドを使用して、キーセットまたはMultiMap自体の防御コピーを使用することを検討する必要があります。unmodifiableMultimapMultiMaps(getTermsメソッドから変更不可能なMultiMapを返す必要があります)。同期されたMultiMapを返す必要性を調査することもできますが、その場合も、基になるコレクションを同時変更から保護するために、ミューテックスをスレッドによって取得する必要があることを確認する必要があります。

実際のコレクションへの同時アクセスが保証されるかどうかわからないという唯一の理由で、スレッドセーフHashSetの使用について言及することを意図的に省略していることに注意してください。ほとんどの場合、そうではありません。


編集:シングルスレッドシナリオでConcurrentModificationExceptionスローされますIterator.next

if(c.isSomething()) C.remove(c);これは、編集された質問で紹介されたステートメントに関するものです。

呼び出すCollection.removeと、質問の性質が変わりますConcurrentModificationException。これは、シングルスレッドのシナリオでもsをスローできるようになるためです。

可能性は、メソッド自体の使用と、Collection'イテレータの使用、この場合itはステートメント:を使用して初期化された変数の使用から生じますIterator<BooleanClause> it = C.iterator();

これは、の現在の状態に関連するストアの状態Iterator itを繰り返し処理します。この特定のケース(Sun / Oracle JREを想定)では、 (によって使用されるクラスの内部内部クラス)を使用して、を反復処理します。これの特別な特徴は、 (この場合は)そのメソッドを介して実行された構造変更の数を追跡することです。Collection CCollectionKeyIteratorHashMapHashSetCollectionIteratorCollectionHashMapIterator.remove

直接呼び出しremoveCollectionから、の呼び出しでフォローアップするとIterator.next、イテレータは、が認識していない構造上の変更が発生したかどうかConcurrentModificationExceptionを確認するために、をスローします。この場合、は構造の変更を引き起こします。これは、によって追跡されますが、によっては追跡されません。Iterator.nextCollectionIteratorCollection.removeCollectionIterator

問題のこの部分を克服するには、を呼び出す必要が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.
                }
            }
        }
于 2011-08-30T02:03:29.337 に答える
8

その理由は、イテレータの外部でコレクションを変更しようとしているためです。

使い方 :

イテレータを作成すると、コレクションはコレクションとイテレータの両方のmodificationNum変数を個別に維持します。1.コレクションの変数は、コレクションとイテレーターに加えられた変更ごとにインクリメントされます。 2.イテレータの変数は、イテレータに加えられた変更ごとにインクリメントされます。

したがってit.remove()、両方のmodification-number-variableの値を1増やすイテレータを介して呼び出す場合。

ただしcollection.remove()、コレクションを直接呼び出すと、コレクションのmodification-number変数の値のみがインクリメントされ、イテレーターの変数はインクリメントされません。

そして、ルールは次のとおりです。イテレータの変更番号の値が元のコレクションの変更番号の値と一致しない場合は常に、ConcurrentModificationExceptionが発生します。

于 2011-08-30T04:48:34.110 に答える
3

ConcurrentModificationExceptionVineet Reynoldsは、コレクションが(スレッドセーフ、同時実行性)をスローする理由を詳細に説明しています。Swagatikaは、このメカニズムの実装の詳細(コレクションとイテレーターが変更の数をカウントする方法)を詳細に説明しています。

彼らの答えは面白かったので、私は彼らに賛成しました。ただし、あなたの場合、問題は同時実行性(スレッドが1つしかない)に起因するものではなく、実装の詳細は興味深いものですが、ここでは考慮しないでください。

HashSetjavadocのこの部分のみを考慮する必要があります。

このクラスのイテレータメソッドによって返されるイテレータはフェイルファストです。イテレータが作成された後、イテレータ自体のremoveメソッド以外の方法でセットが変更された場合、イテレータはConcurrentModificationExceptionをスローします。したがって、同時変更に直面した場合、イテレータは、将来の不確定な時点で任意の非決定的な動作のリスクを冒すのではなく、迅速かつクリーンに失敗します。

コードでは、イテレータを使用してHashSetを反復処理しますが、HashSet独自のremoveメソッドを使用して要素(C.remove(c))を削除します。これにより、が発生しConcurrentModificationExceptionます。代わりに、javadocで説明されているように、Iterator独自のremove()メソッドを使用する必要があります。これにより、基になるコレクションから現在反復されている要素が削除されます。

交換

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

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

より機能的なアプローチを使用したい場合は、述語を作成し、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:どちらの場合も、これは一時的なからのみ要素を削除することに注意してくださいHashSet。はMultimap変更されません。

于 2011-08-30T12:30:03.047 に答える