web-dev-qa-db-ja.com

Java)でのメモリバリアの動作

ブログや記事などをもっと読んだ後、メモリバリアの前後のロード/ストアの動作について本当に混乱しています。

以下は、JMMに関する彼の説明記事の1つにあるDoug Leaからの2つの引用であり、どちらも非常に単純です。

  1. 揮発性フィールドfに書き込むときにスレッドAに表示されていたものはすべて、fを読み取るときにスレッドBに表示されます。
  2. 発生前の関係を適切に設定するには、両方のスレッドが同じ揮発性変数にアクセスすることが重要であることに注意してください。揮発性フィールドfを書き込んだときにスレッドAに表示されるすべてのものが、揮発性フィールドgを読み取った後にスレッドBに表示されるわけではありません。

しかし、メモリバリアについて別の blog を調べたところ、次のようになりました。

  1. x86のストアバリア「sfence」命令は、バリアの前にあるすべてのストア命令をバリアの前に強制的に発生させ、ストアバッファをフラッシュして発行されたCPUのキャッシュに入れます。
  2. x86のロードバリア「lfence」命令は、バリア後のすべてのロード命令をバリアの後に強制的に発生させ、ロードバッファがそのCPUをドレインするのを待ちます。

私にとって、Doug Leaの説明は他の説明よりも厳密です。基本的に、ロードバリアとストアバリアが異なるモニター上にある場合、データの一貫性は保証されません。ただし、後者は、バリアが異なるモニター上にある場合でも、データの一貫性が保証されることを意味します。これら2つを正しく理解しているかどうかはわかりません。また、どちらが正しいかわかりません。

次のコードを検討します。

  public class MemoryBarrier {
    volatile int i = 1, j = 2;
    int x;

    public void write() {
      x = 14; //W01
      i = 3;  //W02
    }

    public void read1() {
      if (i == 3) {  //R11
        if (x == 14) //R12
          System.out.println("Foo");
        else
          System.out.println("Bar");
      }
    }

    public void read2() {
      if (j == 2) {  //R21
        if (x == 14) //R22
          System.out.println("Foo");
        else
          System.out.println("Bar");
      }
    }
  }

1つの書き込みスレッドTW1が最初にMemoryBarrierのwrite()メソッドを呼び出し、次に2つのリーダースレッドTR1とTR2がMemoryBarrierのread1()とread2()メソッドを呼び出すとしましょう。このプログラムが順序を保持しないCPUで実行されていると考えてください(x86そのような場合の順序を保持してください)、メモリモデルによると、W01/W02の間にStoreStoreバリア(たとえばSB1)があり、R11/R12とR21/R22の間に2つのLoadLoadバリアがあります( RB1とRB2と言います)。

  1. SB1とRB1は同じモニターiにあるため、read1を呼び出すスレッドTR1は常にxに14を表示し、「Foo "は常に印刷されます。
  2. SB1とRB2は異なるモニター上にあり、Doug Leaが正しければ、スレッドTR2がxに14を表示することは保証されません。つまり、「バー」がときどき印刷される可能性があります。ただし、メモリバリアが blog で説明されているMartin Thompsonのように実行される場合、ストアバリアはすべてのデータをメインメモリにプッシュし、ロードバリアはすべてのデータをメインメモリからキャッシュ/バッファにプルします。TR2もxで14が表示されることが保証されています。

どちらが正しいか、または両方が正しいかはわかりませんが、MartinThompsonが説明したのはx86アーキテクチャ専用です。 JMMは、xへの変更がTR2に表示されることを保証しませんが、x86の実装は表示します。

ありがとう〜

32
asticx

ダグ・リーは正しいです。関連する部分は、Java言語仕様のセクション §17.4.4 にあります。

§17.4.4同期順序

[..]揮発性変数への書き込みv(§8.3.1.4)同期-with任意のスレッドによるvの後続のすべての読み取り(ここで、「後続」は同期順序に従って定義されます)。 [..]

concrete machineのメモリモデルは重要ではありません。これは、Javaプログラミング言語のセマンティクスがabstract machineで定義されているためです。 = コンクリートマシンから独立。Javaランタイム環境は、次の保証に準拠するようにコードを実行する責任があります。 Java言語仕様


実際の質問について:

  • それ以上の同期がない場合、メソッドread2印刷可能"Bar"、なぜならread2writeの前に実行できます。
  • CountDownLatchとの追加の同期がある場合は、read2が実行されますafterwrite、次にメソッドread2は決して印刷しません"Bar"CountDownLatchとの同期により、xの-​​データ競合が削除されるためです。

独立した揮発性変数:

揮発性変数への書き込みが他の揮発性変数の読み取りと同期しないことは理にかなっていますか?

はい、それは理にかなっています。 2つのスレッドが相互作用する必要がある場合、通常、情報を交換するために同じvolatile変数を使用する必要があります。一方、スレッドが他のすべてのスレッドと対話する必要なしに揮発性変数を使用する場合、メモリバリアのコストを支払う必要はありません。

それは実際には重要です。例を挙げましょう。次のクラスは、揮発性メンバー変数を使用します。

class Int {
    public volatile int value;
    public Int(int value) { this.value = value; }
}

このクラスがメソッド内でローカルにのみ使用されると想像してください。 JITコンパイラは、オブジェクトがこのメソッド内でのみ使用されていることを簡単に検出できます( エスケープ分析 )。

public int deepThought() {
    return new Int(42).value;
}

上記のルールを使用すると、volatile変数は他のスレッドからアクセスできないため、JITコンパイラはvolatileの読み取りと書き込みのすべての影響を取り除くことができます。

この最適化は、実際にはJava JITコンパイラ:

15
nosid

私が理解している限り、質問は実際には揮発性の読み取り/書き込みとその発生-保証前です。その部分について言えば、nosidの答えに追加することが1つだけあります。

揮発性の書き込みは通常の書き込みの前に移動できません。揮発性の読み取りは通常の読み取りの後に移動できません。そのため、read1()およびread2()の結果はnosidが記述したとおりになります。

障壁について言えば、定義は私には問題ないように聞こえますが、おそらくあなたを混乱させる1つのことは、これらがホットスポットでJMMで説明されている動作を実装するためのもの/ツール/方法/メカニズム(好きなように呼んでください)であるということです。 Javaを使用する場合は、実装の詳細ではなく、JMMの保証に依存する必要があります。

1
Alexey Malev