Javaでスレッドセーフセットを生成するさまざまな実装と方法がたくさんあるようです。いくつかの例が含まれます
2) Collections.synchronizedSet(Set set)
4) Collections.newSetFromMap(new ConcurrentHashMap())
5)(4)と同様の方法で生成されたその他のセット
これらの例は、 同時実行パターン:Java 6の同時セット実装
誰かがこれらの例と他の例の違い、利点、欠点を簡単に説明してもらえますか? Java Std Docsからのすべてを理解し、まっすぐに保つのに苦労しています。
1)CopyOnWriteArraySet
は非常に単純な実装です。基本的に配列内に要素のリストがあり、リストを変更すると、配列がコピーされます。この時点で実行されている反復およびその他のアクセスは古い配列で継続され、リーダーとライター間の同期の必要性を回避します(ただし、書き込み自体は同期する必要があります)。ここでは、配列が線形時間で検索されるため、通常高速のセット操作(特にcontains()
)は非常に低速です。
これは、頻繁に読み取られ、めったに変更されない、本当に小さなセットにのみ使用します。 (Swingsリスナーセットは一例ですが、これらは実際にはセットではないため、とにかくEDTからのみ使用する必要があります。)
2)Collections.synchronizedSet
は、元のセットの各メソッドを同期ブロックで単純にラップします。元のセットに直接アクセスしないでください。これは、セットの2つのメソッドを同時に実行できないことを意味します(一方が他方が終了するまでブロックします)-これはスレッドセーフですが、複数のスレッドが実際にセットを使用している場合、同時実行性はありません。イテレータを使用する場合、通常、イテレータ呼び出し間でセットを変更するときにConcurrentModificationExceptionsを回避するために、外部で同期する必要があります。パフォーマンスは、元のセットのパフォーマンスに似ています(ただし、同期オーバーヘッドがいくらかあり、同時に使用した場合はブロックされます)。
同時実行性が低く、すべての変更が他のスレッドからすぐに見えるようにする場合に使用します。
3)ConcurrentSkipListSet
は並行SortedSet
実装であり、O(log n)で最も基本的な操作が行われます。追加/削除と読み取り/反復の同時追加が可能です。反復は、イテレータが作成されてからの変更について通知する場合としない場合があります。バルク操作は単純に複数の単一の呼び出しであり、アトミックではありません-他のスレッドはそれらの一部のみを監視する場合があります。
明らかに、あなたはあなたの要素に何らかの完全な順序がある場合にのみこれを使うことができます。これは、あまり大きくないセット(O(log n)のため)の同時実行性の高い状況の理想的な候補のように見えます。
4)ConcurrentHashMap
(およびそれから派生したSet)の場合:ここで最も基本的なオプションは(平均して、O(1)のhashCode()
が優れている場合) _(ただし、O(n)に縮退する場合があります)、HashMap/HashSetなど。書き込みの同時実行には制限があり(テーブルはパーティション化され、書き込みアクセスは必要なパーティションで同期されます)、読み取りアクセスはそれ自体と書き込みスレッドに対して完全に同時実行されます(ただし、現在の変更の結果はまだ表示されない場合があります)書かれた)。イテレータは、作成されてからの変更が表示される場合と表示されない場合があり、バルク操作はアトミックではありません。サイズ変更は遅い(HashMap/HashSetの場合)ので、作成時に必要なサイズを見積もることでこれを回避しようとします(3/4がいっぱいになるとサイズが変更されるので、さらに約1/3を使用します)。
大きなセット、適切な(そして高速な)ハッシュ関数があり、マップを作成する前にセットサイズと必要な同時実行性を推定できる場合にこれを使用します。
5)ここで使用できる他の並行マップ実装はありますか?
AtomicReference<Set>
を使用し、各変更でセット全体を置き換えることにより、HashSet
のcontains()
パフォーマンスをCopyOnWriteArraySet
の同時実行関連のプロパティと組み合わせることができます。
実装スケッチ:
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;
}
}
}
}
Javadocが役に立たない場合は、おそらくデータ構造について読むための本や記事を見つける必要があります。一目で:
別の工夫は、スレッドセーフな weak reference のセットです。
このようなセットは、 pub-sub シナリオでサブスクライバーを追跡するのに便利です。サブスクライバーが他の場所でスコープ外に出て、ガベージコレクションの候補になる場合、サブスクライバーはスムーズにサブスクライブを解除する必要はありません。弱参照により、サブスクライバはガベージコレクションの候補への移行を完了することができます。ガベージが最終的に収集されると、セット内のエントリが削除されます。
バンドルされたクラスではそのようなセットは直接提供されませんが、数回の呼び出しでセットを作成できます。
まず、 Set
クラスを利用して、弱い参照のWeakHashMap
を作成します。これは Collections.newSetFromMap
のクラスドキュメントに示されています。
Set< YourClassGoesHere > weakHashSet =
Collections
.newSetFromMap(
new WeakHashMap< YourClassGoesHere , Boolean >()
)
;
マップのKeyがBoolean
を構成するため、マップのValueSet
はここでは無関係です。
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
が期限切れのエントリをクリアする時期と方法の説明と例については、この質問を参照してください * WeakHashMapは増え続けていますか、またはガベージキーをクリアしますか? * 。