11

申し訳ありませんが、これはとても長い質問です。

私は最近、マルチスレッドを個人的なプロジェクトにゆっくりと実装するため、マルチスレッドについて多くの研究を行ってきました。ただし、おそらくわずかに不正確な例が豊富にあるため、特定の状況での同期ブロックの使用とボラティリティは、私にはまだ少し不明確です。

私の中心的な質問はこれです:スレッドが同期されたブロック内にあるとき、参照とプリミティブへの変更は自動的に揮発性です(つまり、キャッシュではなくメインメモリで実行されます)、または読み取りも同期する必要がありますか?ちゃんと?

  1. もしそうなら、単純なゲッターメソッドを同期する目的は何ですか?(例1を参照)また、スレッドが何かで同期している限り、すべての変更がメインメモリに送信されますか?たとえば、非常に高レベルの同期内のあらゆる場所で大量の作業を行うために送信された場合、すべての変更はメインメモリに行われ、再びロックが解除されるまでキャッシュには何も行われませんか?
  2. そうでない場合、変更は同期ブロック内で明示的に行う必要がありますか、それともJavaは、たとえばLockオブジェクトの使用を実際に取得できますか?(例3を参照)
  3. 同期されたオブジェクトは、何らかの方法で変更されている参照/プリミティブに関連付ける必要がありますか(たとえば、それを含む直接のオブジェクト)?あるオブジェクトを同期して書き込み、それ以外の場合は安全であれば別のオブジェクトと読み取ることはできますか?(例2を参照)

(以下の例では、同期されたメソッドと同期された(これ)が嫌われていることとその理由を知っていますが、それについての議論は私の質問の範囲を超えています)

例1:

class Counter{
  int count = 0;

  public synchronized void increment(){
    count++;
  }

  public int getCount(){
    return count;
  }
}

この例では、++はアトミック操作ではないため、increment()を同期する必要があります。そのため、2つのスレッドが同時に増加すると、全体でカウントが1増加する可能性があります。カウントプリミティブはアトミックである必要があり(たとえば、long / double / referenceではない)、それで問題ありません。

getCount()をここで同期する必要がありますか?その理由は正確ですか?私が最もよく聞いた説明は、返されるカウントがインクリメント前かインクリメント後かを保証するものではないということです。しかし、これは少し違う何かの説明のように思えます、それはそれ自体が間違った場所にあることに気づきました。つまり、getCount()を同期したとしても、保証はありません。実際の読み取りが実際の書き込みの前後であるかどうかがわからないので、ロックの順序がわからないことになります。

例2:

次の例は、ここに示されていないトリックを通じて、これらのメソッドのいずれも同時に呼び出されることはないと想定した場合、スレッドセーフですか?毎回ランダムな方法を使用してカウントが増加し、その後適切に読み取られる場合、カウントは期待どおりに増加しますか、それともロック同じオブジェクトである必要がありますか?(ところで、私はこの例がいかにばかげているかを完全に理解していますが、私は実践よりも理論に興味があります)

class Counter{
  private final Object lock1 = new Object();
  private final Object lock2 = new Object();
  private final Object lock3 = new Object();
  int count = 0;

  public void increment1(){
    synchronized(lock1){
      count++;
    }
  }

  public void increment2(){
    synchronized(lock2){
      count++;
    }
  }

  public int getCount(){
    synchronized(lock3){
      return count;
    }
  }

}

例3:

起こる前の関係は単にJavaの概念ですか、それともJVMに組み込まれている実際のものですか?この次の例では、概念的な発生前の関係を保証できますが、Javaは、組み込みのものであれば、それを理解するのに十分賢いですか?そうではないと思いますが、この例は実際にはスレッドセーフですか?スレッドセーフの場合、getCount()がロックしなかった場合はどうでしょうか。

class Counter{
  private final Lock lock = new Lock();
  int count = 0;

  public void increment(){
    lock.lock();
    count++;
    lock.unlock();
  }

  public int getCount(){
    lock.lock();
    int count = this.count;
    lock.unlock();
    return count;
  }
}
4

2 に答える 2

8

はい、読み取りも同期する必要があります。このページは言う:

あるスレッドによる書き込みの結果は、読み取り操作の前に、書き込み操作が発生した場合にのみ、別のスレッドによる読み取りに表示されることが保証されます。

[...]

モニターのロック解除(同期ブロックまたはメソッド終了)が発生します-同じモニターの後続のすべてのロック(同期ブロックまたはメソッド入力)の前に

同じページには次のように書かれています。

Lock.unlock、Semaphore.release、CountDownLatch.countDownなどのシンクロナイザーメソッドを「解放」する前のアクションは、Lock.lockなどの「取得」メソッドが成功した後のアクションの前に発生します。

したがって、ロックは同期されたブロックと同じ可視性の保証を提供します。

同期されたブロックまたはロックのどちらを使用する場合でも、リーダースレッドがライタースレッドと同じモニターまたはロックを使用する場合にのみ、可視性が保証されます。

  • 例1は正しくありません。カウントの最新の値を確認する場合は、ゲッターも同期する必要があります。

  • 例2は、同じカウントを保護するために異なるロックを使用しているため、正しくありません。

  • 例3はOKです。ゲッターがロックされなかった場合は、カウントの古い値が表示される可能性があります。発生前は、JVMによって保証されているものです。JVMは、たとえばキャッシュをメインメモリにフラッシュするなど、指定されたルールを尊重する必要があります。

于 2012-05-28T07:55:10.447 に答える
6

2つの異なる単純な操作の観点からそれを表示してみてください。

  1. ロック(相互排除)、
  2. メモリバリア(キャッシュ同期、命令の並べ替えバリア)。

ブロックに入るにsynchronizedは、ロックとメモリバリアの両方が必要です。ブロックを離れると、synchronizedロック解除とメモリバリアが必要になります。フィールドの読み取り/書き込みには、volatileメモリバリアのみが必要です。これらの用語で考えると、上記のすべての質問を自分で明確にできると思います。

例1の場合、読み取りスレッドにはいかなる種類のメモリバリアもありません。読み取りの前後で値を確認するだけでなく、スレッドの開始後に変数の変更を確認しないことも重要です。

例2は、あなたが提起する最も興味深い問題です。この場合、JLSからの保証はありません。実際には、順序の保証はありませんが(ロックの側面がまったくないかのように)、最初の例とは異なり、メモリバリアの利点があり、変化を観察できます。synchronized基本的に、これはasを削除してタグ付けするのとまったく同じです(ロックを取得するための実行時のコストは別として)intvolatile

例3に関しては、「Javaのことだけ」で、静的コードチェックだけが認識している消去を念頭に置いたジェネリックを持っていると思います。これはそのようなものではありません。ロックとメモリバリアはどちらも純粋なランタイムアーティファクトです。実際、コンパイラーはそれらについてまったく推論できません。

于 2012-05-28T07:52:54.333 に答える