2

set-with-a-map スレッドセーフを利用するクラスを作成しようとしています。what特に同期する必要があるかどうかはわかりません。

マップは に似たものとして定義されますMap<Class<K>, Set<V>> map;。以下は、マップが実装で内部的に使用されている方法の縮小です。

public void addObject(K key, V object) {
    getSet(key).add(object);
}

public void removeObject(K key, V object) {
    getSet(key).remove(object);
}

public void iterateObjectsInternally(K key, Object... params)
{
    for (V o : getSet(key)) {
        o.doSomething(params);
    }
}

private Set<V> getSet(K key) {
    if (!map.containsKey(key)) {
        map.put(key, new Set<V>());
    }

    return map.get(key);
}

マップの問題

自体を使用する限り、map私が目にする唯一の同時実行性の問題はgetSet(K)、スレッドコンテキストが と の間で切り替わる可能性がある にcontainsKeyありputます。この場合、次のことが発生する可能性があります。

[Thread A] map.containsKey(key)       => returns false
[Thread B] map.containsKey(key)       => returns false
[Thread B] map.put(key, new Set<V>())
[Thread B] map.get(key).add(object)
[Thread A] map.put(key, new Set<V>()) => Thread A ovewrites Thread B's object [!]
[Thread B] map.get(key).add(object)

今、私は現在HashMap、この実装に定期的に使用しています。そして、私が正しければ、Collection.synchronizedMap()orConcurrentHashMapを使用すると、メソッドレベルでの同時実行の問題のみが解決されます。つまり、メソッドはアトミックに実行されます。これらは、メソッドが互いに相互作用する方法について何も述べていないため、並行ソリューションを使用している場合でも、次のことが発生する可能性があります。

ConcurrentHashMapただし、メソッドがありputIfAbsentます。これの欠点はmap.putIfAbsent(key, new Set<V>())、セットが要求されるたびにステートメントが新しいセットを作成することです。これは多くのオーバーヘッドのようです。

一方で、これら 2 つのステートメントを同期ブロックでラップするだけで十分でしょうか?

synchronized(map) {
    if (!map.containsKey(key)) {
        map.put(key, new Set<V>());
    }
}

マップ全体をロックするよりも良い方法はありますか? マップの他の値の読み取りがロックアウトされないように、キーのみをロックする方法はありますか?

synchronized(key) {
    if (!map.containsKey(key)) {
        map.put(key, new Set<V>());
    }
}

キーは必ずしも同じオブジェクトである必要はありませんが (具体的にはClass<?>型です)、ハッシュコードは等しいことに注意してください。同期にkeyオブジェクト アドレスの同等性が必要な場合、同期は機能しない可能性があります。

セットの問題

より大きな問題は、セットが適切に使用されているかどうかを知ることだと思います. オブジェクトの追加、オブジェクトの削除、オブジェクトの反復など、いくつかの問題があります。

と での同時実行の問題を回避するには、リストをラップするCollections.synchronizedListだけで十分でしょうか? 同期されたラッパーはそれらをアトミック操作にするので、それで問題ないと思います。addObjectremoveObject

ただし、繰り返しは別の話かもしれません。の場合iterateObjectsInternally、セットが同期されている場合でも、外部で同期する必要があります。

Set<V> set = getSet(key);
synchronized(set) {
    for (V value : set) {
        // thread-safe iteration
    }
}

しかし、これはひどい無駄のように思えます。代わりに、単純にCopyOnWriteArrayListorCopyOnWriteArraySetを定義として置き換えたらどうなるでしょうか。反復では単に配列の内容のスナップショットを使用するだけなので、別のスレッドから変更する方法はありません。また、CopyOnWriteArrayList追加および削除メソッドで再入可能ロックを使用します。これは、追加/削除も本質的に安全であることを意味します (それらは同期されたメソッドであるため)。CopyOnWriteArrayList内部構造の反復回数がリストの変更回数を大幅に上回っているため、魅力的に見えます。また、コピーされた反復子を使用すると、別のスレッドで( ) からの反復を心配しaddObjectたり、removeObject台無しにしたりする必要がありません。iterateObjectInternallyConcurrentModificationExceptions

これらの並行性チェックは正しい軌道に乗っているか、および/または十分に厳密ですか? 私は並行プログラミングの問題を抱えた初心者であり、明らかな何かを見落としているか、考えすぎている可能性があります。同様の質問がいくつかあることは知っていますが、私の実装は、私と同じように具体的に質問することを正当化するのに十分なほど異なっているように見えました。

4

2 に答える 2

1

あなたは間違いなくこれを考えすぎています。同時実行性の特性に応じて、単純な ConcurrentHashMap と ConcurrentSkipListSet/CopyOnWriteArraySet を使用します (主に反復でデータのオンザフライ変更を考慮する必要がある場合)。次のスニペットのようなものを getSet メソッドとして使用します。

private Set<V> getSet(K key) {
    Set<V> rv = map.get(key);
    if (rv != null) {
        return rv;
    }
    map.putIfAbsent(key, new Set<V>());
    return map.get(key);
}

これにより、オブジェクトの追加/削除時に適切なロックレス同時実行が保証されます。繰り返しのために、問題のドメインで更新の欠落が問題であるかどうかを判断する必要があります。反復中に追加された新しいオブジェクトを見逃すことが問題にならない場合は、CopyOnWriteArraySet を使用します。

3 番目に、同時実行に関してどのような粒度を使用できるか、要件は何か、エッジ ケースでの適切な動作は何か、そして何よりも、コードが必要とするパフォーマンスと同時実行の特性について詳しく調べたいと考えています。カバー - 起動時に 2 回発生する場合は、すべてのメソッドを同期させて処理を完了します。

于 2012-08-24T00:53:34.843 に答える
0

'set'に頻繁に追加する場合、CopyOnWriteArrayListとCopyOnWriteArraySetは実行可能ではありません-追加操作に非常に多くのリソースを使用します。ただし、追加することがめったになく、「セット」を頻繁に繰り返す場合は、最善の策です。

Java ConcurrentHashMapは、すべてのマップをバケット自体に配置します。putif absent操作を実行すると、キーの検索時にリストがロックされ、ロックを解除してキーを挿入します。マップの代わりにConcurrentHashMapを使用してください。

getSetメソッドは、特に同期されている場合、本質的に遅くなる可能性があります。おそらく、すべてのキーとセットを後でより早くプリロードすることができます。

Louis Wassermanの発言に沿って、Guavaの実装でパフォーマンスが適切かどうかを確認することをお勧めします。

于 2012-08-24T00:55:54.560 に答える