Java メモリ モデルの概要

Java メモリ モデル (JMM) は、 Java ランタイム環境におけるスレッドの動作を記述します。メモリ モデルは Java 言語のセマンティクスの一部であり、特定の Java マシン用ではなく Java 全体用のソフトウェアを開発するときにプログラマが期待できることとすべきでないことを記述します。

1995 年に開発されたオリジナルの Java メモリ モデル (特に「パーコローカル メモリ」を指します) は失敗とみなされています。コードの安全性の保証を失わずに多くの最適化を行うことはできません。特に、マルチスレッドの「シングル」を記述するためのオプションがいくつかあります。

  • シングルトンにアクセスするすべての行為 (オブジェクトがずっと前に作成され、何も変更できない場合でも) によってスレッド間ロックが発生します。
  • または、特定の状況下では、システムは未完成のロンナーを発行します。
  • または、特定の状況下では、システムは 2 人の孤立者を作成します。
  • または、設計は特定のマシンの動作に依存します。

そこで、メモリー機構を再設計しました。2005 年の Java 5 のリリースで新しいアプローチが発表され、Java 14 のリリースでさらに改良されました。

新しいモデルは 3 つのルールに基づいています。

ルール 1 : シングルスレッド プログラムは擬似的に順次実行されます。つまり、実際には、プロセッサはクロックごとに複数の操作を実行し、同時に順序を変更できますが、データの依存関係はすべて残るため、動作はシーケンシャルと変わりません。

ルール 2 : 突然の値は存在しません。変数を読み取ると (この規則が適用されない可能性がある不揮発性の long と double を除く)、デフォルト値 (ゼロ) か、別のコマンドによってそこに書き込まれた値が返されます。

そしてルール番号 3 : 残りのイベントは、厳密な半順序関係「前に実行される」 (前に発生する) によって接続されている場合、順番に実行されます。

前に起こる

レスリー・ランポートは以前に「Happens」というコンセプトを思いつきました。これはアトミック コマンド間に導入された厳密な半順序関係 (++ と -- はアトミックではありません) であり、「物理的に前」を意味するものではありません。

2番目のチームは最初のチームによって行われた変更を「知っている」と述べています。

前に起こる

たとえば、次のような操作では、一方が他方より先に実行されます。

同期と監視:

  • モニター (ロックメソッド、同期開始) と、その後に同じスレッドで発生するものをキャプチャします。
  • モニターの戻り (メソッドlock、 synchronized の終了) と、その前に同じスレッドで起こったこと。
  • モニターを返し、別のスレッドでキャプチャします。

書くことと読むこと:

  • 任意の変数に書き込み、同じストリームでそれを読み取ります。
  • volatile 変数に書き込む前のすべてと書き込み自体が同じスレッド内にあります。volatile 読み取りとその後のすべてが同じスレッド上にあります。
  • 揮発性変数に書き込み、再度読み取ります。揮発性書き込みはモニターの戻りと同じ方法でメモリと対話しますが、読み取りはキャプチャに似ています。1 つのスレッドが揮発性変数に書き込み、2 番目のスレッドがそれを見つけた場合、書き込みに先立つすべての処理が、読み取りの後のすべての処理よりも前に実行されることがわかります。写真を参照してください。

オブジェクトのメンテナンス:

  • 静的初期化とオブジェクトのインスタンスを使用したアクション。
  • コンストラクターの最終フィールドとコンストラクター以降のすべてのフィールドに書き込みます。例外として、前発生関係は他のルールに推移的に接続しないため、スレッド間競合が発生する可能性があります。
  • オブジェクトとFinalize()を操作します。

ストリームサービス:

  • スレッドとスレッド内のコードを開始します。
  • スレッドおよびスレッド内のコードに関連する変数をゼロにします。
  • スレッドとjoin()のコード。スレッド内のコードとisAlive() == false
  • スレッドにinterrupt()を実行し、スレッドが停止したことを検出します。

仕事の前に起こるニュアンス

前発生モニターの解放は、同じモニターを取得する前に行われます。これは終了ではなく解放であることに注意してください。つまり、wait を使用するときに安全性を心配する必要はありません。

この知識が例の修正にどのように役立つかを見てみましょう。この場合、すべては非常に簡単です。外部チェックを削除し、同期をそのままにするだけです。2 番目のスレッドは、他のスレッドがモニターを解放した後でのみモニターを取得するため、すべての変更を確認できることが保証されます。そして、すべてが初期化されるまでリリースしないので、すべての変更を個別にではなく一度に確認します。

public class Keeper {
    private Data data = null;

    public Data getData() {
        synchronized(this) {
            if(data == null) {
                data = new Data();
            }
        }

        return data;
    }
}

volatile 変数への書き込みは、同じ変数からの読み取りの前に行われます。もちろん、私たちが加えた変更によりバグは修正されますが、元のコードを書いた人は元の場所に戻され、毎回ブロックされてしまいます。volatile キーワードは保存できます。実際、問題のステートメントは、揮発性と宣言されたものをすべて読み取ると、常に実際の値が取得されることを意味します。

さらに、前に述べたように、揮発性フィールドの場合、書き込みは常に (long と double を含む) アトミックな操作です。もう 1 つの重要な点: 他のエンティティ (配列、リスト、その他のクラスなど) への参照を持つ揮発性エンティティがある場合、エンティティ自体への参照だけが常に「新鮮」になりますが、エンティティ内のすべての参照が常に「新鮮」になるわけではありません。入ってきます。

さて、ダブルロックラムの話に戻りましょう。volatile を使用すると、次のように状況を修正できます。

public class Keeper {
    private volatile Data data = null;

    public Data getData() {
        if(data == null) {
            synchronized(this) {
                if(data == null) {
                    data = new Data();
                }
            }
        }
        return data;
    }
}

ここでもロックは残っていますが、それは data == null の場合のみです。残りのケースは volatile read を使用して除外します。正確さは、揮発性ストアが揮発性読み取りの前に発生し、フィールドの値を読み取る人にはコンストラクター内で発生するすべての操作が見えるという事実によって保証されます。