Javaのさまざまなタイプのスレッドセーフセット


135

Javaでスレッドセーフなセットを生成するには、さまざまな実装と方法がたくさんあるようです。いくつかの例が含まれます

1)CopyOnWriteArraySet

2)Collections.synchronizedSet(セットセット)

3)ConcurrentSkipListSet

4)Collections.newSetFromMap(new ConcurrentHashMap())

5)(4)と同様の方法で生成されたその他のセット

これらの例は、並行性パターンから来ています:Java 6の並行セット実装

誰かがこれらの例や他の例の違い、利点、欠点を簡単に説明できますか?Java Std Docsのすべてを理解し、まっすぐに保つのに問題があります。

回答:


206

1)CopyOnWriteArraySetは非常に単純な実装です。基本的には配列に要素のリストがあり、リストを変更すると配列がコピーされます。この時点で実行されている反復とその他のアクセスは、古いアレイで続行され、読み取りと書き込みの間の同期の必要性を回避します(ただし、書き込み自体は同期する必要があります)。contains()配列は線形時間で検索されるため、通常は高速なセット操作(特に)はここではかなり遅くなります。

これは、頻繁に読み込まれ(繰り返し)、めったに変更されない非常に小さなセットにのみ使用してください。(Swingsリスナーセットは一例ですが、実際にはセットではなく、EDTからのみ使用する必要があります。)

2)Collections.synchronizedSet元のセットの各メソッドの周りに同期ブロックをラップするだけです。元のセットに直接アクセスしないでください。つまり、セットの2つのメソッドを同時に実行することはできません(一方は他方が完了するまでブロックされます)。これはスレッドセーフですが、複数のスレッドが実際にセットを使用している場合、同時実行性はありません。イテレータを使用する場合、イテレータの呼び出し間でセットを変更するときにConcurrentModificationExceptionsを回避するために、通常は引き続き外部と同期する必要があります。パフォーマンスは、元のセットのパフォーマンスに似ています(ただし、同期オーバーヘッドがあり、同時に使用するとブロックされます)。

同時実行性が低く、すべての変更が他のスレッドにすぐに見えるようにしたい場合に、これを使用します。

3)ConcurrentSkipListSetは並行SortedSet実装であり、O(log n)で最も基本的な演算が行われます。イテレータが作成されてからの変更について、反復によって通知される場合と通知されない場合とで、追加/削除と読み取り/反復を同時に実行できます。バルク操作は単純に複数の単一の呼び出しであり、アトミックではありません。他のスレッドはそれらの一部のみを監視する場合があります。

明らかに、これは、要素に全体的な順序がある場合にのみ使用できます。これは、(O(log n)のため)大きすぎないセットに対して、同時実行性の高い状況の理想的な候補のように見えます。

4)ConcurrentHashMap(およびそれから導出されたセット)の場合:ここで最も基本的なオプションはhashCode()、H(Map)のように、O(1)の(平均して、適切で高速な場合)です(ただし、O(n)に縮退する可能性があります)。 HashSet。書き込みの同時実行には制限があります(テーブルはパーティション化されており、書き込みアクセスは必要なパーティションで同期されます)一方で、読み取りアクセスはそれ自体と書き込みスレッドに完全に並行します(ただし、現在行われている変更の結果はまだ表示されない場合があります)書かれた)。イテレータは、作成されてからの変更を確認する場合としない場合があり、一括操作はアトミックではありません。サイズ変更が遅い(HashMap / HashSetの場合と同様)ため、作成時に必要なサイズを見積もることでこれを回避しようとします(3/4フルになるとサイズ変更されるため、約1/3をさらに使用します)。

大きなセットと適切な(そして高速な)ハッシュ関数があり、マップを作成する前にセットのサイズと必要な同時実行性を推定できる場合に、これを使用します。

5)ここで使用できる他の並行マップ実装はありますか?


1
1)の視力矯正では、データを新しいアレイにコピーするプロセスは、同期化によってロックダウンする必要があります。したがって、CopyOnWriteArraySetは、同期の必要性を完全に回避しません。
CaptainHastings

ConcurrentHashMap基づいて、セット、「これ作成に必要なサイズを推定することにより、これを回避しよう。」セットは75%の負荷でサイズ変更されるため、マップに指定するサイズは、推定(または既知の値)より33%以上大きくする必要があります。私が使用するexpectedSize + 4 / 3 + 1
Daren

@Daren私は最初の推測+であることを意味しますか*
–PaŭloEbermann、2015

@PaŭloEbermannもちろん...なるはずですexpectedSize * 4 / 3 + 1
Daren

1
以下のためにConcurrentMap(またはHashMap)は、Java 8(私はそれが16であると信じて)同じバケットに到達しきい値にマッピングするエントリの数は、リストは、バイナリ検索ツリー(precisedする赤黒木)にその場合のルックアップで変更入った場合時間はO(lg n)ありませんO(n)
akhil_mittal 2017

20

を使用し、各変更でセット全体を置き換えることにより、のcontains()パフォーマンスとHashSet同時実行性に関連するプロパティを組み合わせることができます。CopyOnWriteArraySetAtomicReference<Set>

実装スケッチ:

public abstract class CopyOnWriteSet<E> implements Set<E> {

    private final AtomicReference<Set<E>> ref;

    protected CopyOnWriteSet( Collection<? extends E> c ) {
        ref = new AtomicReference<Set<E>>( new HashSet<E>( c ) );
    }

    @Override
    public boolean contains( Object o ) {
        return ref.get().contains( o );
    }

    @Override
    public boolean add( E e ) {
        while ( true ) {
            Set<E> current = ref.get();
            if ( current.contains( e ) ) {
                return false;
            }
            Set<E> modified = new HashSet<E>( current );
            modified.add( e );
            if ( ref.compareAndSet( current, modified ) ) {
                return true;
            }
        }
    }

    @Override
    public boolean remove( Object o ) {
        while ( true ) {
            Set<E> current = ref.get();
            if ( !current.contains( o ) ) {
                return false;
            }
            Set<E> modified = new HashSet<E>( current );
            modified.remove( o );
            if ( ref.compareAndSet( current, modified ) ) {
                return true;
            }
        }
    }

}

実際AtomicReferenceに値を揮発性としてマークします。つまりhappens-before、コンパイラが古いコードを並べ替えることができないため、古いデータを読み取るスレッドがないことを確認し、保証を提供します。しかし、get / setメソッドのみAtomicReferenceが使用されている場合、実際には、変数を揮発性のある方法で揮発性にマークしています。
akhil_mittal 2017

(1)私が何かを見落とさない限り、すべてのコレクションタイプで機能します(2)他のどのクラスも、一度にコレクション全体をアトミックに更新する方法を提供しないため、この回答は十分に賛成できません...これは非常に便利です。
ギリ

私はこの逐語的表現を流用しようとしましたがabstract、いくつかのメソッドを記述する必要を回避するために、ラベルが付いていることがわかりました。私はそれらを追加しようとしましたが、でロードブロッキングに遭遇しましたiterator()。モデルを壊さずに、このことについてイテレーターを維持する方法がわかりません。私は常にを通過するref必要があるようですが、毎回異なる基本セットを取得する可能性があります。これには、基本ゼロセットから新しいイテレータを取得する必要があります。洞察はありますか?
nclark

確かに、各顧客は時間内に固定のスナップショットを取得するので、基になるコレクションのイテレータが必要な場合は問題なく動作します。私の使用例は、競合するスレッドがその中の個々のリソースを「要求」できるようにすることです。異なるバージョンのセットがある場合は機能しません。2番目に...私のスレッドは、新しいイテレータを取得して、CopyOnWriteSet.remove(chosen_item)がfalseを返した場合に再試行する必要があると思います...それは関係なく行う必要があります:)
nclark

11

Javadocsが役に立たない場合は、おそらくデータ構造について読むための本または記事を見つける必要があります。一目で:

  • CopyOnWriteArraySetは、コレクションを変更するたびに、基になる配列の新しいコピーを作成するため、書き込みが遅く、イテレータは高速で一貫しています。
  • Collections.synchronizedSet()は、昔ながらの同期されたメソッド呼び出しを使用してSetをスレッドセーフにします。これはパフォーマンスの低いバージョンになります。
  • ConcurrentSkipListSetは、一貫性のないバッチ操作(addAll、removeAllなど)とイテレータを使用したパフォーマンスの高い書き込みを提供します。
  • Collections.newSetFromMap(new ConcurrentHashMap())にはConcurrentHashMapのセマンティクスがあり、必ずしも読み取りまたは書き込み用に最適化されているとは限りませんが、ConcurrentSkipListSetと同様に、バッチ操作に一貫性がありません。

1
developer.com/java/article.php/10922_3829891_2/… <本よりも優れています)
ycomp

1

弱参照の同時セット

もう1つの工夫は、スレッド参照弱い参照のセットです。

このようなセットは、pub-subシナリオで加入者を追跡するのに便利です。サブスクライバーが他の場所で範囲外になり、そのためガベージコレクションの候補となる場合、サブスクライバーは適切にサブスクライブを解除することに煩わされる必要はありません。弱参照により、サブスクライバーはガベージコレクションの候補になるための移行を完了することができます。ガベージが最終的に収集されると、セット内のエントリは削除されます。

バンドルされたクラスにそのようなセットは直接提供されていませんが、数回の呼び出しでセットを作成できます。

最初にSetWeakHashMapクラスを利用して弱参照を作成することから始めます。これはのクラスドキュメントに示されていますCollections.newSetFromMap

Set< YourClassGoesHere > weakHashSet = 
    Collections
    .newSetFromMap(
        new WeakHashMap< YourClassGoesHere , Boolean >()
    )
;

マップのキーがを構成するため、マップのBooleanここでは無関係です。Set

pub-subなどのシナリオでは、サブスクライバーとパブリッシャーが別々のスレッドで動作している場合(かなりの場合)、スレッドセーフが必要です。

同期セットとしてラップして、このセットをスレッドセーフにすることにより、さらに一歩進んでください。への呼び出しにフィードしますCollections.synchronizedSet

this.subscribers =
        Collections.synchronizedSet(
                Collections.newSetFromMap(
                        new WeakHashMap <>()  // Parameterized types `< YourClassGoesHere , Boolean >` are inferred, no need to specify.
                )
        );

これで、結果のにサブスクライバーを追加および削除できますSet。また、「消えた」サブスクライバーは、ガベージコレクションの実行後に最終的に自動的に削除されます。この実行がいつ発生するかは、JVMのガベージコレクターの実装に依存し、現時点での実行時の状況に依存します。基礎となるものWeakHashMapが期限切れのエントリをいつ、どのようにクリアするかについての説明と例については、この質問を参照してください。*

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.