9

マルチスレッド環境に非同期のJavaコレクションがあり、コレクションのリーダーを強制的に同期させたくない場合[1]、ライターを同期して参照割り当てのアトミック性を使用するソリューションは実行可能ですか?何かのようなもの:

private Collection global = new HashSet(); // start threading after this

void allUpdatesGoThroughHere(Object exampleOperand) {
  // My hypothesis is that this prevents operations in the block being re-ordered
  synchronized(global) {
    Collection copy = new HashSet(global);
    copy.remove(exampleOperand);
    // Given my hypothesis, we should have a fully constructed object here. So a 
    // reader will either get the old or the new Collection, but never an 
    // inconsistent one.
    global = copy;    
  }
}

// Do multithreaded reads here. All reads are done through a reference copy like:
// Collection copy = global;
// for (Object elm: copy) {...
// so the global reference being updated half way through should have no impact 

このような状況では、独自のソリューションをロールすることは失敗することが多いようです。そのため、データコンシューマーのオブジェクトの作成とブロックを防ぐために使用できる他のパターン、コレクション、またはライブラリについて知りたいと思います。


[1]デッドロックが発生するリスクと相まって、書き込みに比べて読み取りに費やされる時間の割合が高い理由。


編集:いくつかの回答とコメントの多くの良い情報、いくつかの重要なポイント:

  1. 投稿したコードにバグがありました。グローバル(不適切な名前の変数)で同期すると、スワップ後に同期されたブロックを保護できない場合があります。
  2. クラスで同期する(synchronizedキーワードをメソッドに移動する)ことでこれを修正できますが、他のバグがある可能性があります。より安全で保守しやすい解決策は、java.util.concurrentの何かを使用することです。
  3. 私が投稿したコードには「結果整合性の保証」はありません。リーダーがライターによる更新を確認できるようにする1つの方法は、volatileキーワードを使用することです。
  4. 振り返ってみると、この質問の動機となった一般的な問題は、Javaでロックされた書き込みを使用してロックのない読み取りを実装しようとしたことでしたが、私の(解決された)問題はコレクションにあり、将来の読者を不必要に混乱させる可能性があります。したがって、私が投稿したコードが機能することが明らかでない場合は、一度に1人のライターが、複数のリーダースレッドによって保護されずに読み取られている「オブジェクト」の編集を実行できるようにします。編集のコミットはアトミック操作によって行われるため、読者は編集前または編集後の「オブジェクト」のみを取得できます。リーダースレッドが更新を取得した場合、「オブジェクト」の古いコピーで読み取りが行われているため、読み取りの途中で更新を行うことはできません。Javaでより優れた並行性サポートが利用可能になる前に、おそらく発見され、何らかの方法で壊れていることが証明された単純なソリューション。
4

5 に答える 5

13

独自のソリューションを展開しようとするのではなく、ConcurrentHashMapをセットとして使用し、すべての値を標準値に設定してみませんか?(のような定数Boolean.TRUEがうまく機能します。)

この実装は、多くの読者と少数の作家のシナリオでうまく機能すると思います。予想される「同時実行レベル」を設定できるコンストラクターもあります。

更新: Veerは、 Collections.newSetFromMapユーティリティメソッドを使用してConcurrentHashMapをセットに変換することを提案しました。この方法はMap<E,Boolean>私の推測ではあるので、すべての値をBoolean.TRUE舞台裏に設定しても同じことを行うと思います。


更新:投稿者の例に対処する

それはおそらく私が最終的に行くことになるでしょうが、私はまだ私のミニマリストの解決策がどのように失敗する可能性があるかについて興味があります。–マイルハンプソン

最小限のソリューションは、少し調整するだけで問題なく機能します。私の心配は、現在は最小限ですが、将来はもっと複雑になるかもしれないということです。スレッドセーフなものを作成するときに想定するすべての条件を覚えておくのは困難です。特に、数週間/数か月/数年後にコードに戻って、一見取るに足らない微調整を行う場合はなおさらです。ConcurrentHashMapが必要なすべてを十分なパフォーマンスで実行する場合は、代わりにそれを使用してみませんか?厄介な同時実行の詳細はすべてカプセル化されており、6か月後でも、それを台無しにするのに苦労するでしょう!

現在のソリューションが機能する前に、少なくとも1つの微調整が必​​要です。すでに指摘したように、おそらくの宣言にvolatile修飾子を追加する必要があります。あなたがC/C ++のバックグラウンドを持っているかどうかはわかりませんが、Javaのセマンティクスが実際にはCよりもはるかに複雑でglobalあることを知ったときは非常に驚きました。Javaで多くの並行プログラミングを行うことを計画している場合は、Javaメモリモデルの基本に慣れておくことをお勧めします。参照への参照を作成しない場合、スレッドが値を更新しようとするまで、スレッドがの値の変更を確認できない可能性があります。更新を試みると、volatile globalvolatileglobalsynchronizedブロックはローカルキャッシュをフラッシュし、更新された参照値を取得します。

ただし、追加してもvolatile大きな問題があります。2つのスレッドの問題シナリオは次のとおりです。

  1. 空集合、またはから始めますglobal={}。スレッドABその両方は、スレッドローカルのキャッシュメモリにこの値を持っています。
  2. スレッドはロックをA取得し、新しいキーのコピーを作成してセットに追加することにより、更新を開始します。synchronizedglobalglobal
  3. ThreadAがまだsynchronizedブロック内にある間、 ThreadはBそのローカル値をglobalスタックに読み取り、ブロックに入ろうとしますsynchronized。スレッドAは現在モニターのスレッドBブロック内にあるため。
  4. スレッドAは、参照を設定してモニターを終了することで更新を完了し、結果としてglobal={1}
  5. これで、スレッドBはモニターに入り、global={1}セットのコピーを作成できるようになります。
  6. スレッドAは別の更新を行うことを決定し、ローカル参照を読み込んでブロックglobalに入ろうとします。スレッドBは現在ロックを保持しているため、ロックはオンにならず、スレッドは正常にモニターに入ります。synchronized{}{1}A
  7. スレッドは、更新のためにAのコピーも作成します。{1}

これで、スレッドABは両方ともsynchronizedブロック内にあり、セットの同一のコピーがありglobal={1}ます。これは、更新の1つが失われることを意味します。synchronizedこの状況は、ブロック内で更新している参照に格納されているオブジェクトで同期しているという事実が原因です。同期に使用するオブジェクトには常に注意を払う必要があります。この問題は、ロックとして機能する新しい変数を追加することで修正できます。

private volatile Collection global = new HashSet(); // start threading after this
private final Object globalLock = new Object(); // final reference used for synchronization

void allUpdatesGoThroughHere(Object exampleOperand) {
  // My hypothesis is that this prevents operations in the block being re-ordered
  synchronized(globalLock) {
    Collection copy = new HashSet(global);
    copy.remove(exampleOperand);
    // Given my hypothesis, we should have a fully constructed object here. So a 
    // reader will either get the old or the new Collection, but never an 
    // inconsistent one.
    global = copy;    
  }
}

このバグは十分に潜行的であったため、他のどの回答もまだ対処していません。自分で何かをまとめようとするのではなく、すでにデバッグされているjava.util.concurrentライブラリから何かを使用することをお勧めするのは、このようなクレイジーな並行性の詳細です。上記の解決策はうまくいくと思いますが、それを再び台無しにするのはどれほど簡単でしょうか?これはとても簡単です:

private final Set<Object> global = Collections.newSetFromMap(new ConcurrentHashMap<Object,Boolean>());

参照はfinal古い参照を使用するスレッドについて心配する必要がないため、またConcurrentHashMap内部ですべての厄介なメモリモデルの問題を処理するため、モニターやメモリバリアの厄介な詳細について心配する必要はありません。

于 2012-08-15T04:28:44.600 に答える
7

関連するJavaチュートリアルによると、

のような増分式c++は、アトミックアクションを記述しないことはすでに見てきました。非常に単純な式でも、他のアクションに分解できる複雑なアクションを定義できます。ただし、アトミックであると指定できるアクションがあります。

  • long読み取りと書き込みは、参照変数およびほとんどのプリミティブ変数(およびを除くすべてのタイプdouble)に対してアトミックです。
  • 読み取りと書き込みは、宣言されたすべての変数volatile(および変数を含む )に対してアトミックです。longdouble

これは、Java言語仕様のセクション§17.7によって再確認されています。

参照の書き込みと読み取りは、32ビット値と64ビット値のどちらとして実装されているかに関係なく、常にアトミックです。

実際、参照アクセスがアトミックであることに依存できるようです。ただし、これは、すべてのリーダーがこの書き込み後に更新された値を読み取ることを保証するものではないことを認識してください。つまり、ここではメモリオーダリングの保証はありません。global

synchronizedへのすべてのアクセスで暗黙的なロックを使用する場合は、globalここでメモリの一貫性を構築できます...ただし、別のアプローチを使用する方がよい場合があります。

globalまた、コレクションを不変のままにしておきたいようです...幸いなことに、Collections.unmodifiableSetこれを強制するために使用できるものがあります。例として、次のようなことを行う必要があります...

private volatile Collection global = Collections.unmodifiableSet(new HashSet());

...それ、またはを使用してAtomicReference

private AtomicReference<Collection> global = new AtomicReference<>(Collections.unmodifiableSet(new HashSet()));

次に、変更したコピーにも使用Collections.unmodifiableSetします。


// ... All reads are done through a reference copy like:
// Collection copy = global;
// for (Object elm: copy) {...
// so the global reference being updated half way through should have no impact

for (Object elm : global)内部的に次のように作成されるため、ここでコピーを作成することは冗長であることを知っておく必要がありIteratorます...

final Iterator it = global.iterator();
while (it.hasNext()) {
  Object elm = it.next();
}

globalしたがって、読書の最中に、まったく異なる値に切り替える可能性はありません。


それはさておき、私はDaoWenによって表現された感情に同意します...で利用可能な代替手段java.util.concurrentがあるかもしれないときにあなたがここであなた自身のデータ構造を転がしている理由はありますか?生の型を使用しているので、おそらく古いJavaを扱っていると思いましたが、質問しても問題ありません。

CopyOnWriteArrayList、またはそのいとこ(前者を使用してCopyOnWriteArraySet実装する)によって提供されるコピーオンライトコレクションのセマンティクスを見つけることができます。Set


DaoWenからも提案されましたが、?の使用を検討しましたConcurrentHashMapか?forこれらは、例で行ったようにループを使用することで一貫性があることを保証します。

同様に、イテレータと列挙は、イテレータ/列挙の作成時または作成以降のある時点でのハッシュテーブルの状態を反映する要素を返します。

内部的には、Iteratorを拡張forするために使用されIterableます。

次のようSetに利用することで、これからを作成できます。Collections.newSetFromMap

final Set<E> safeSet = Collections.newSetFromMap(new ConcurrentHashMap<E, Boolean>());
...
/* guaranteed to reflect the state of the set at read-time */
for (final E elem : safeSet) {
  ...
}
于 2012-08-15T04:16:24.917 に答える
1

あなたの最初のアイデアは健全だったと思います。DaoWenはバグを取り除くのに良い仕事をしました。あなたがあなたのためにすべてをする何かを見つけることができない限り、いくつかの魔法のクラスがあなたのためにそれをすることを望むよりも、これらのことを理解する方が良いです。魔法のクラスはあなたの生活を楽にし、間違いの数を減らすことができますが、あなたは彼らが何をしているのかを理解したいと思います。

ConcurrentSkipListSetは、ここであなたのためにより良い仕事をするかもしれません。マルチスレッドの問題をすべて取り除くことができます。

ただし、HashSetよりも低速です(通常、HashSetsとSkipLists / Treesを比較するのは困難です)。書き込みごとに多くの読み取りを行う場合、得られるものはより高速になります。さらに重要なことに、一度に複数のエントリを更新すると、読み取りに一貫性のない結果が表示される可能性があります。エントリAがあるときはいつでもエントリBがあると予想する場合、またはその逆の場合は、スキップリストで一方が他方なしで表示される可能性があります。

現在のソリューションでは、読者にとって、マップの内容は常に内部的に一貫しています。読み取りにより、すべてのBにAがあることを確認できます。size() メソッドが、イテレーターによって返される要素の正確な数を提供することを確認できます。2回繰り返すと、同じ要素が同じ順序で返されます。

つまり、allUpdatesGoThroughHereとConcurrentSkipListSetは、2つの異なる問題に対する2つの優れたソリューションです。

于 2012-08-15T20:25:06.630 に答える
-1

synchronizedを作成して置き換えれglobal volatileば、コピーオンライトに関しては問題ありません。

割り当てはアトミックですが、他のスレッドでは、参照されるオブジェクトへの書き込みで順序付けられていません。読み取りと書き込みの両方を同期する、または同期する前に発生する関係が必要です。volatile

一度に複数の更新が発生するという問題は別です。単一のスレッドまたはそこで実行したいことは何でも使用してください。

読み取りと書き込みの両方にを使用した場合synchronized、それは正しいでしょうが、読み取りをハンドオフする必要があるため、パフォーマンスが良くない場合があります。AReadWriteLockが適切な場合もありますが、読み取りをブロックする書き込みがあります。

パブリケーションの問題に対する別のアプローチは、最終フィールドのセマンティクスを使用して、(理論的には)安全でないオブジェクトを作成することです。

もちろん、同時コレクションも利用できます。

于 2012-08-15T04:09:41.607 に答える
-1

この方法を使用できますCollections.synchronizedSetか?HashSetJavadocからhttp://docs.oracle.com/javase/6/docs/api/java/util/HashSet.html

Set s = Collections.synchronizedSet(new HashSet(...));
于 2012-08-15T04:15:35.870 に答える