Double-Checkにおけるvolatile作用

3047 ワード

「張新強」のブログから転載
完璧に見える単例モード?
public class Single {
    private static Single3 instance;
    private Single() {}
    public static Single getInstance() {
        if (instance == null) {
            synchronized (Single.class) {
                if (instance == null) {
                    instance = new Single3();
                }
            }
        }
        return instance;
    }
}
  • の最初のif(instance==null)は、効率の問題を解決するために、instanceがnullの場合にのみsynchronizedのコードセグメントに入る--確率が大幅に減少した.
  • の2番目のif(instance==null)は、複数のインスタンスが発生する可能性を防止するためである.

  • では、また問題がありますか?
    答え:『見た目』だけで、まだ小さな確率で問題が発生します.

    げんしさぎょ


    原子操作(atomic)は分割不可能な操作であり,コンピュータではスレッドスケジューリングによって中断されない操作を指す.
    m = 6; //  
    

    mの元の値が0である場合、この操作に対して、実行に成功したmが6になったか、mが実行されていないか0になったかのいずれかであり、m=3のような中間状態は、同時スレッドにおいても現れない.
    一方、宣言して値を割り当てるのは原子操作ではありません.
    int n = 6; //  
    

    この文について、少なくとも2つの操作があります.1つの変数n 2がnに6を割り当てます.これにより、変数nが宣言されていますが、まだ割り当てられていない状態が中間状態になります.--このように、マルチスレッドでは、スレッド実行順序の不確実性のため、両方のスレッドがmを使用すると、不安定な結果が生じる可能性がある.

    めいれいさいはいち


    コンセプト
    簡単に言えば、コンピュータは実行効率を高めるためにいくつかの最適化を行い、最終結果に影響を与えない場合、いくつかの文の実行順序を調整する可能性があります.
    int a ;   //  1 
    a = 8 ;   //  2
    int b = 9 ;     //  3
    int c = a + b ; //  4
    

    通常、シーケンス構造の場合、実行されるシーケンスは、上から下、すなわち1234である.ただし、命令の並び替えのため、最終的な結果に影響を及ぼさないため、実際に実行される順序が3124または1324になる可能性がある.文3と4には原子性の問題がないため、文3と文4も原子操作に分割され、再配置される可能性があります.--つまり、
    非原子的な操作では、最終結果に影響を及ぼさない場合、分割された原子操作が実行順序を再配置される可能性がある.

    話題にもどる


    主にsingleton=new Singleton()という文にありますが、これは原子操作ではありません.実際にJVMでは次の3つのことをしています.
  • singletonにメモリを割り当てる
  • Singletonのコンストラクタを呼び出してメンバー変数を初期化し、インスタンス
  • を形成する.
  • singletonオブジェクトを割り当てられたメモリ領域(singletonがnullではない)
  • に指し示します.
    ただし、JVMのインスタント・コンパイラでは、コマンドの再ソートの最適化が存在します.すなわち、上記の2ステップ目と3ステップ目の順序は保証されず、最終的な実行順序は1−2−3であっても1−3−2であってもよい.後者であれば、3実行完了、2未実行の前にスレッド2によってプリエンプトされ、このときinstanceは既にnullではない(ただし初期化されていない)ので、スレッド2は直接instanceに戻り、使用し、そのままエラーを報告します.
    もう少し説明すると、『instanceはnullではないがまだ初期化が完了していない』という中間状態があるので、このとき、他のスレッドがちょうど第1層if(instance==null)まで動作している場合、ここで読み取ったinstanceはnullではないので、この中間状態のinstanceをそのまま持って行って使用すると、問題が発生します.ここで重要なのは、スレッドT 1のinstanceに対する書き込みが完了していないことであり、スレッドT 2は読み取り操作を実行することである.

    完全版

    public class Single {
        private static volatile Single4 instance;
        private Single() {}
        public static Single getInstance() {
            if (instance == null) {
                synchronized (Single.class) {
                    if (instance == null) {
                        instance = new Single();
                    }
                }
            }
            return instance;
        }
    }
    

    volatileが果たす役割


    volatileキーワードの1つの役割は、命令の再配置を禁止し、instanceをvolatileと宣言すると、その書き込み操作にメモリバリア(メモリバリアとは何ですか?)があります.これにより、その付与が完了する前に、読み取り操作を呼び出す必要はありません.
    注意:volatileがブロックするのはsingleton=new Singleton()という文の内部[1-2-3]の命令の再配置ではなく、1つの書き込み操作([1-2-3])が完了するまで読み取り操作を呼び出さないことを保証します(if(instance==null)).