12

私の知る限り、volatile writeは volatile readの前に発生する ため、常に最新のデータが volatile 変数に表示されます。私の質問は基本的に、発生する前に発生するという用語と、それがどこで発生するかに関するものですか? 質問を明確にするためにコードを書きました。

class Test {
   volatile int a;
   public static void main(String ... args) {
     final Test t = new Test();
     new Thread(new Runnable(){
        @Override
        public void run() {
            Thread.sleep(3000);
            t.a = 10;
        }
     }).start();
     new Thread(new Runnable(){
        @Override
        public void run() {
            System.out.println("Value " + t.a);
        }
     }).start();
   }
}

(明確にするために try catch ブロックは省略されています)

この場合、常に値 0 がコンソールに表示されます。なしThread.sleep(3000);では、常に値10が表示されます。これは、発生する前の関係の場合ですか、それともスレッド1がスレッド2より少し早く開始するため、「値10」を出力しますか?

上記のコードの結果は、(少なくとも私の場合は) スレッドの順序とスレッドのスリープにのみ依存するため、volatile 変数を使用する場合と使用しない場合のコードの動作がプログラムの開始ごとに異なる例を見るのは素晴らしいことです。

4

6 に答える 6

9

書き込みの前に読み取りが実行されるため、値 0 が表示されます。また、読み取りの前に書き込みが実行されるため、値 10 が表示されます。

より予測不可能な出力を伴うテストが必要な場合は、両方のスレッドが同時に開始されるように、両方のスレッドが CountDownLatch を待機するようにする必要があります。

final CountDownLatch latch = new CountDownLatch(1);
new Thread(new Runnable(){
    @Override
    public void run() {
        try {
            latch.await();
            t.a = 10;
        }
        catch (InterruptedException e) {
            // end the thread
        }
    }
 }).start();
 new Thread(new Runnable(){
    @Override
    public void run() {
        try {
            latch.await();
            System.out.println("Value " + t.a);
        }
        catch (InterruptedException e) {
            // end the thread
        }
    }
 }).start();
 Thread.sleep(321); // go
 latch.countDown();
于 2012-06-04T21:07:36.973 に答える
5

先行発生は、後続の読み取りの前に書き込みが発生することに実際に関係しています。書き込みがまだ行われていない場合、実際には何の関係もありません。書き込みスレッドがスリープしているため、書き込みが発生する前に読み取りが実行されます。

実際の関係を観察するために、揮発性の変数とそうでない変数の 2 つの変数を使用できます。JMM によると、揮発性の読み取りの前に、揮発性の書き込みが発生する前に、不揮発性の変数への書き込みが行われます。

例えば

volatile int a = 0;
int b = 0;

スレッド 1:

b = 10;
a = 1;

スレッド 2:

while(a != 1);
if(b != 10)
  throw new IllegalStateException();

Java メモリ モデルではb、不揮発性ストアが揮発性ストアの前に発生するため、常に 10 に等しくなければならないと言われています。また、揮発性ストアの前に 1 つのスレッドで発生するすべての書き込みは、後続のすべての揮発性ロードの前に発生します。

于 2012-06-04T21:09:11.437 に答える
1

「前に起こる」という用語に固執しないでください。これはイベント間の関係であり、R/W 操作のスケジューリング中に jvm によって使用されます。この段階では、揮発性を理解するのに役立ちません。ポイントは、jvm がすべての R/W 操作を注文することです。jvm は必要に応じて注文できます (もちろん、すべての同期、ロック、待機などに従います)。そして今:変数が揮発性である場合、読み取り操作は最新の書き込み操作の結果を参照します。variable が volatile でない場合、保証されません (別のスレッドで)。それで全部です

于 2012-06-04T21:16:50.913 に答える
1

よりよく理解できるように、質問の最初の文で言及されている事前発生規則を次のように言い直しました (太字フォントの変更)。

「メインメモリへの揮発性変数の値の書き込みは、メインメモリからのその変数の後続の読み取りの 前に発生します」。

  • また、揮発性の書き込み/読み取りは常にメインメモリとの間で発生し、レジスタ、プロセッサキャッシュなどのローカルメモリリソースとの間では発生しないことに注意することが重要です.

上記の先行発生規則の実際的な意味は、volatile 変数を共有するすべてのスレッドが常にその変数の一貫した値を参照するということです。任意の時点で、2 つのスレッドがその変数の異なる値を参照することはありません。

逆に、非揮発性変数を共有するすべてのスレッドは、同期されたブロック/メソッド、final キーワードなどの他の種類の同期メカニズムによって同期されていない限り、任意の時点で異なる値を参照する可能性があります。

この事前発生ルールに関する質問に戻りますが、そのルールを少し誤解していると思います。このルールは、書き込みコードが常に読み取りコードの前に発生 (実行) する必要があることを示しているわけではありません。むしろ、あるスレッドで書き込みコード (揮発性変数書き込み) が別のスレッドでの読み取りコードの前に実行される場合、読み取りコードが実行されるに書き込みコードの効果がメイン メモリで発生する必要があることを指示します。読み取りコードは最新の値を見ることができます。

volatile (またはその他の同期メカニズム) が存在しない場合、これは必須ではありません。そのため、読み取りスレッドは、別の書き込みスレッドによって最近書き込まれたものであっても、非 volatile 変数の古い値を参照する可能性があります。書き込みスレッドはローカル コピーに値を格納でき、値をメイン メモリにフラッシュする必要がないためです。

上記の説明が明確であることを願っています:)

于 2013-02-18T11:27:00.337 に答える
0

piotrekは正しいです、ここにテストがあります:

class Test {
   volatile int a = 0;
   public static void main(String ... args) {
     final Test t = new Test();
     new Thread(new Runnable(){
        @Override
        public void run() {
            try {
                Thread.sleep(3000);
            } catch (Exception e) {}
            t.a = 10;
            System.out.println("now t.a == 10");
        }
     }).start();
     new Thread(new Runnable(){
        @Override
        public void run() {
            while(t.a == 0) {}
            System.out.println("Loop done: " + t.a);
        }
     }).start();
   }
}

揮発性の場合:常に終了します

揮発性なし:それは決して終わらない

于 2012-08-17T00:35:54.663 に答える
0

ウィキから:

特に Java では、先行発生関係は、ステートメント A によって書き込まれたメモリがステートメント B から見えること、つまり、ステートメント B が読み取りを開始する前にステートメント A が書き込みを完了することを保証するものです。

そのため、スレッド A が値 10 で ta を書き込み、スレッド B がその後に ta を読み取ろうとすると、先行発生関係により、スレッド B はスレッド A によって書き込まれた値 10 を読み取らなければならず、他の値は読み取れないことが保証されます。アリスが牛乳を買って冷蔵庫に入れ、ボブが冷蔵庫を開けて牛乳を見るのと同じように、それは自然なことです. ただし、コンピュータが実行されている場合、通常、メモリ アクセスは直接メモリにアクセスすることはなく、遅すぎます。代わりに、ソフトウェアはレジスターまたはキャッシュからデータを取得して時間を節約します。キャッシュミスが発生した場合にのみメモリからデータをロードします。問題が発生すること。

質問のコードを見てみましょう。

class Test {
  volatile int a;
  public static void main(String ... args) {
    final Test t = new Test();
    new Thread(new Runnable(){ //thread A
      @Override
      public void run() {
        Thread.sleep(3000);
        t.a = 10;
      }
    }).start();
    new Thread(new Runnable(){ //thread B
      @Override
      public void run() {
        System.out.println("Value " + t.a);
      }
    }).start();
  }
}

スレッド A は値 ta に 10 を書き込み、スレッド B はそれを読み取ろうとします。スレッド A がスレッド B を読み取る前に書き込むとします。スレッド B が読み取ると、レジスタまたはキャッシュに値をキャッシュしないため、メモリから値がロードされ、スレッド A によって常に 10 が書き込まれるとします。スレッド B が読み取り、スレッド B が初期値 (0) を読み取ります。したがって、この例では、volatile の動作とその違いは示されていません。しかし、次のようにコードを変更すると:

class Test {
  volatile int a;
  public static void main(String ... args) {
    final Test t = new Test();
    new Thread(new Runnable(){ //thread A
      @Override
      public void run() {
        Thread.sleep(3000);
        t.a = 10;
      }
    }).start();
    new Thread(new Runnable(){ //thread B
      @Override
      public void run() {
        while (1) {
          System.out.println("Value " + t.a);
        }
      }
    }).start();
  }
}

volatileを使用しない場合、スレッド A が ta に 10 を書き込んだ後に何らかの読み取りが発生したとしても、出力値は常に初期値 (0) である必要があり、これは発生前の関係に違反します。その理由は、コンパイラがコードを最適化し、ta をレジスタに保存し、キャッシュ メモリから読み取る代わりにレジスタ値を使用するたびに、もちろんはるかに高速になるためです。しかし、他のスレッドが更新した後にスレッド B が正しい値を取得できないため、先行発生関係違反の問題も発生します。

上記の例では、volatile 書き込みが volatile 読み取りの前に発生するということは、スレッド A が更新した後にvolatileスレッド B が ta の正しい値を取得することを意味します。コンパイラは、スレッド B が ta を読み取るたびに、レジスタの古い値を使用するだけでなく、キャッシュまたはメモリから読み取る必要があることを保証します。

于 2013-05-31T12:23:23.453 に答える