サーチ…


備考

Javaメモリモデルは、あるスレッドが別のスレッドによって行われたメモリ書き込みの影響を見ることが保証される条件を指定するJLSのセクションです。最近のエディションの関連セクションは、「JLS 17.4メモリモデル」( Java 8Java 7Java 6

Java 5では、Javaメモリモデルの大幅な見直しが行われました。これは(とりわけ) volatileの仕方が変化しました。それ以来、メモリモデルは基本的に変更されていません。

メモリモデルの動機づけ

次の例を考えてみましょう。

public class Example {
    public int a, b, c, d;
    
    public void doIt() {
       a = b + 1;
       c = d + 1;
    }
}

このクラスがシングルスレッドのアプリケーションで使用されている場合、観察可能な振る舞いは期待どおりになります。例えば:

public class SingleThreaded {
    public static void main(String[] args) {
        Example eg = new Example();
        System.out.println(eg.a + ", " + eg.c);
        eg.doIt();
        System.out.println(eg.a + ", " + eg.c);
    }
}

出力されます:

0, 0
1, 1

"main"スレッドが知る限りmain()メソッドとdoIt()メソッドのdoIt()は、ソースコードで書かれた順序で実行されます。これは、Java言語仕様(JLS)の明確な要件です。

次に、マルチスレッドアプリケーションで使用される同じクラスについて考えてみましょう。

public class MultiThreaded {
    public static void main(String[] args) {
        final Example eg = new Example();
        new Thread(new Runnable() {
            public void run() {
                while (true) {
                    eg.doIt();
                }
            }
        }).start();
        while (true) {
            System.out.println(eg.a + ", " + eg.c);
        }
    }
}

これは何を印刷しますか?

実際、JLSによれば、これが印刷されることを予測することはできません:

  • おそらく0, 0までの数行が表示されます。
  • 次にN, NN, N + 1ます。
  • N + 1, Nような行が表示されることがあります。
  • 理論的には、あなたもいることを表示される場合があります0, 0行が1永遠に続けます。

1 - 実際には、 printlnステートメントの存在により、ある程度の予期せぬ同期とメモリキャッシュのフラッシュが発生する可能性があります。これは、上記の動作を引き起こすいくつかの影響を隠す可能性があります。

では、どうやって説明するのですか?

割り当ての並べ替え

予期しない結果の可能性のある説明の1つは、JITコンパイラがdoIt()メソッドの代入順序を変更したことです。 JLSでは、ステートメント 現在のスレッドの観点から順番実行されるように指定する必要があります。この場合、 doIt()メソッドのコードでは、これら2つのステートメントの(仮説的な)並べ替えの効果を観察することはできません。これは、JITコンパイラが許可されることを意味します。

それはなぜでしょうか?

典型的な現代的なハードウェアでは、機械命令は、命令パイプラインを使用して実行され、一連の命令が異なる段階にあることを可能にする。命令実行のフェーズの中には他のものより時間がかかり、メモリ操作に時間がかかる傾向があります。スマートコンパイラは、オーバーラップの量を最大にする命令を順序付けることによって、パイプラインの命令スループットを最適化することができます。これにより、ステートメントの一部が順不同で実行される可能性があります。 JLS は、現在のスレッドの観点から計算の結果に影響を与えないように、これを許可します

メモリキャッシュの効果

2番目に考えられる説明はメモリキャッシングの効果です。古典的なコンピュータアーキテクチャでは、各プロセッサには小さなレジスタセットと大量のメモリがあります。レジスタへのアクセスはメインメモリへのアクセスよりもはるかに高速です。現代のアーキテクチャでは、レジスタよりも遅いがメインメモリよりも速いメモリキャッシュが存在する。

コンパイラは変数のコピーをレジスタまたはメモリキャッシュに保持しようとすることでこれを利用します。変数は、メインメモリにフラッシュされるように、またはメモリから読み出される必要はありません必要がない場合は、これをやっていないの大幅なパフォーマンス上の利点があります。 JLSがメモリ操作を別のスレッドから見える必要がない場合、Java JITコンパイラは主メモリの読み込みと書き込みを強制する「読み込みバリア」と「書き込みバリア」命令を追加しない可能性があります。もう一度、これを実行することによるパフォーマンス上の利点は重要です。

適切な同期

これまでのところ、JLSはJITコンパイラがメモリ操作を並べ替えたり回避したりすることでシングルスレッドコードを高速化するコードを生成できるようになりました。しかし、他のスレッドがメインメモリ内の(共有の)変数の状態を観察できるとどうなりますか?

その答えは、他のスレッドは、Javaステートメントのコード順に基づいて不可能と思われる可変状態を観察する可能性があるということです。これに対する解決策は、適切な同期を使用することです。主な3つのアプローチは次のとおりです。

  • 原始的なmutexとsynchronizedされた構造体の使用。
  • volatile変数の使用。
  • より高いレベルの並行処理サポートを使用する。例えば、 java.util.concurrentパッケージ内のクラス

しかし、これでも、同期が必要な場所と、あなたが頼りにできる効果を理解することが重要です。これは、Javaメモリモデルが登場する場所です。

メモリモデル

Javaメモリモデルは、あるスレッドが別のスレッドによって行われたメモリ書き込みの影響を見ることが保証される条件を指定するJLSのセクションです。メモリモデルは公平な正式な厳格さで規定されており、(結果として)理解するためには詳細で慎重な読解が必要です。しかし、基本的な原則は、特定の構造体が、あるスレッドによる変数の書き込みと、別のスレッドによる同じ変数の後続の読み込みとの間に「起こる(before-before)」関係を作り出すことである。 「起こる」関係が存在する場合、JITコンパイラーは、読み取り操作が書き込みによって書き込まれた値を確実に参照するコードを生成することを義務付けられています。

このことを踏まえれば、Javaプログラムのメモリコヒーレンシーを推論し、これがすべての実行プラットフォームで予測可能で一貫性があるかどうかを判断することができます。

関係の前に起こること

(以下は、Java言語仕様の簡略化されたバージョンです。より深い理解のためには、仕様自体を読む必要があります)。

事後関係は、メモリモデルの一部であり、メモリの可視性を理解して推論することができます。 JLSが言うように( JLS 17.4.5 ):

「2つのアクションは、 先験的な関係によって注文することができます。あるアクションが起こった場合、別のアクションが起こると 、最初のアクションは2番目のアクションの前に表示され、順序付けられます。

これは何を意味するのでしょうか?

行動

上記の引用が参照するアクションは、 JLS 17.4.2で規定されています。仕様で定義されている5種類のアクションがあります。

  • 読み取り:不揮発性変数を読み込みます。

  • 書き込み:不揮発性変数を書き込む。

  • 同期アクション:

    • 揮発性読み取り:揮発性変数を読み取ります。

    • 揮発性書き込み:揮発性変数を書き込む。

    • ロック。モニターのロック

    • ロックを解除します。モニターのロックを解除する。

    • スレッドの(合成)最初と最後のアクション。

    • スレッドを開始するアクション、またはスレッドが終了したことを検出するアクション。

  • 外部アクション。プログラムが実行される環境に依存する結果を持つアクション。

  • スレッドの分岐アクション。これらは、ある種の無限ループの動作をモデル化します。

プログラム順序と同期順序

これらの2つの順序( JLS 17.4.3およびJLS 17.4.4 )は、Javaのステートメントの実行を制御します

プログラム順序は、単一のスレッド内での文の実行順序を記述する。

同期順序は、同期によって接続された2つのステートメントのステートメント実行の順序を記述します。

  • モニター上のロック解除アクションは、そのモニター上のすべての後続のロック・アクションと同期します。

  • 揮発性変数への書き込みは、後続のすべてのスレッドによる同じ変数の読み取りと同期します。

  • スレッドを開始するアクション(つまりThread.start()への呼び出しThread.start()は、開始するスレッドの最初のアクション(つまりスレッドのrun()メソッドの呼び出し)と同期します。

  • フィールドのデフォルトの初期化は、各スレッドの最初のアクションと同期します。 (これについては、JLSを参照してください)。

  • スレッド内の最後のアクションは、終了を検出した別のスレッド内のアクションと同期します。 trueを返すjoin()呼び出しやisTerminated()呼び出しの戻り値などtrue

  • あるスレッドが別のスレッドに割り込むと、そのスレッドが中断したことを他のスレッドが検出た時点で、最初のスレッドの割り込み呼び出しが同期します。

注文前に起こる

この順序付け( JLS 17.4.5 )は、メモリ書込みが後続のメモリ読取りで見えることが保証されるかどうかを決定するものです。

具体的には、変数の読み取りvへの書き込みを観察することが保証されてv及び場合だけwrite(v) 事前発生 read(v)とに介在書き込みはありませんv 。書き込みが介在する場合、 read(v)は以前の書き込みではなく、その結果を見ることができます。

発生前順序を定義するルールは次のとおりです。

  • ルール#1の前に起こること - xとyが同じスレッドのアクションであり、xがプログラム順序でyの前に来る場合、xはyの前に発生します。

  • ルール#2より前のこと - オブジェクトのコンストラクタの終わりからそのオブジェクトのファイナライザの開始までの先行エッジがあります。

  • ルール3の前に起こる - アクションxが後続のアクションy と同期する場合、x yの前に発生する

  • ルール#4の前に 起こること - x が起きると、 yとy が起こる前、つまりzのにx が起こり、 zの前に起こります。

また、Javaの標準ライブラリ内の様々なクラスを定義として事前発生関係が規定されています。これは、保証がどのように達成されるかを正確に知る必要なく、 何らかの形で起こることを意味すると解釈できます。

起こること - いくつかの例に推論が適用される前に

私たちは、書き込みが、後続の読み込みに表示されていることを確認するために事前発生推論を適用する方法を示すために、いくつかの例を紹介します。

シングルスレッドコード

期待どおり、書き込みは、シングルスレッドプログラムでの後続の読み取りでは常に表示されます。

public class SingleThreadExample {
    public int a, b;
    
    public int add() {
       a = 1;         // write(a)
       b = 2;         // write(b)
       return a + b;  // read(a) followed by read(b)
    }
}

事件の前にルール#1の前に:

  1. write(a)アクションが起こる前に、 write(b)アクション。
  2. write(b)アクションread(a)アクションの前に発生します。
  3. read(a)アクションread(a)アクションの前に発生します。

ルール#4の前に起こったこと:

  1. write(a) が起こる前に、 write(b)およびwrite(b) が起こる前に、 read(a) IMPLIES write(a) 発生し、前に read(a)
  2. write(b) の前に、起こる read(a) AND read(a) が起こる前に、 read(b) IMPLIES write(b) 事前発生 read(b)

要約:

  1. write(a) 事前発生 read(a)関係があることを意味しa + b文は正しい値を参照することが保証されて。 a
  2. write(b) 事前発生 read(b)関係があることを意味しa + b文は正しい値を参照することが保証されてb

2つのスレッドを持つ例における 'volatile'の振る舞い

以下のサンプルコードを使用して、メモリモデルの `volatileの意味を探る。

public class VolatileExample {
    private volatile int a;
    private int b;         // NOT volatile
    
    public void update(int first, int second) {
       b = first;         // write(b)
       a = second;         // write-volatile(a)
    }

    public int observe() {
       return a + b;       // read-volatile(a) followed by read(b)
    }
}

まず、2つのスレッドを含む以下のステートメントのシーケンスを考えてみましょう。

  1. VolatileExample単一のインスタンスが作成されます。それを呼び出すve
  2. ve.update(1, 2)が1つのスレッドで呼び出され、
  3. ve.observe()は別のスレッドで呼び出されます。

事件の前にルール#1の前に:

  1. write(a)アクションvolatile-write(a)アクションの前に発生します。
  2. volatile-read(a)アクションread(b)アクションの前に発生します。

ルール2よりも前に起こったこと:

  1. 第1スレッドのvolatile-write(a)アクションは、第2スレッドのvolatile-read(a)アクションの前に発生します。

ルール#4の前に起こったこと:

  1. 第1スレッドのwrite(b)アクションは、第2スレッドのread(b)アクションの前に発生します。

言い換えれば、この特定のシーケンスでは、第2のスレッドが第1のスレッドによって作られた不揮発性変数bへの更新を見ることが保証される。しかし、それはまた、割り当て場合に明らかであるであるupdate方法は、他の方法で回避した、またはobserve()メソッドは、変数読み取るb前次に起こる、前にチェーンが破壊されることになります。 a第2のスレッドのvolatile-read(a)が第1のスレッドのvolatile-write(a)に続いていない場合、チェーンも破損します。

チェーンが壊れている場合、 observe()に正しい値のbが表示されるという保証はありませ

3つのスレッドで揮発性

前述の例に3番目のスレッドを追加するとします。

  1. VolatileExample単一のインスタンスが作成されます。それを呼び出すve
  2. 2つのスレッドがupdate呼び出します。
    • ve.update(1, 2)が1つのスレッドで呼び出され、
    • ve.update(3, 4)が第2のスレッドで呼び出され、
  3. ve.observe()はその後第3のスレッドで呼び出されます。

これを完全に分析するには、スレッド1とスレッド2のステートメントの可能なインターリーブをすべて考慮する必要があります。代わりに、それらのうちの2つだけを検討します。

シナリオ#1 - update(1, 2)update(3,4)先行してこのシーケンスを取得すると仮定します:

write(b, 1), write-volatile(a, 2)     // first thread
write(b, 3), write-volatile(a, 4)     // second thread
read-volatile(a), read(b)             // third thread

この場合、 write(b, 3)からread(b) まで連続して起こっていない連鎖あることは容易に分かります。さらに、 bへの書き込みはありません。したがって、このシナリオでは、3番目のスレッドはbが値3を持つことを保証します。

シナリオ#2 - update(1, 2)update(3,4)が重なっており、次のようにインタリーブされているとします。

write(b, 3)                           // second thread
write(b, 1)                           // first thread
write-volatile(a, 2)                  // first thread
write-volatile(a, 4)                  // second thread
read-volatile(a), read(b)             // third thread

さて、 write(b, 3)からread(b) までの直前の連鎖がある間に 、他のスレッドによって実行される介在するwrite(b, 1)アクションがあります。これは、 read(b)がどの値を参照するかを特定できないことを意味します。

(これは、非常に限られた状況を除いて、不揮発性変数の可視volatileを確保するためにvolatileに依存することができないことを示しています。)

メモリモデルを理解する必要性を回避する方法

メモリモデルは理解しにくく、適用するのが難しいです。マルチスレッドコードの正当性を推論する必要がある場合に便利ですが、記述するすべてのマルチスレッドアプリケーションに対してこの推論を行う必要はありません。

Javaで並行コードを記述する際に以下のプリンシパルを採用すれば、事故前の推論に頼る必要はほとんどなくなります。

  • 可能であれば、不変のデータ構造を使用してください。正しく実装された不変のクラスはスレッドセーフであり、他のクラスと一緒に使用するときにスレッドセーフの問題が発生しません。

  • 「安全でない公開」を理解し、回避する。

  • プリミティブミューテックスまたは使用Lockスレッドセーフ1である必要は可変オブジェクト状態へのアクセスを同期するためにオブジェクトを。

  • 直接マネージスレッドを作成するのではなく、 Executor / ExecutorServiceまたはfork join frameworkを使用してください。

  • wait / notify / notifyAllを直接使用する代わりに、高度なロック、セマフォ、ラッチ、バリアを提供するjava.util.concurrentクラスを使用してください。

  • 非並行コレクションの外部同期ではなく、マップ、セット、リスト、キューおよびキューのjava.util.concurrentバージョンを使用します。

一般的な原則は、「独自の」並行処理を実行するのではなく、Javaの組み込み同時実行ライブラリを使用することです。あなたがそれらを適切に使用するならば、彼らが働いていることに頼ることができます。


1 - すべてのオブジェクトがスレッドセーフである必要はありません。たとえば、1つまたは複数のオブジェクトがスレッド制限されている場合 (つまり、1つのスレッドのみがアクセス可能な場合)、スレッドセーフティは関係ありません。



Modified text is an extract of the original Stack Overflow Documentation
ライセンスを受けた CC BY-SA 3.0
所属していない Stack Overflow