同時-volatile(二)

6588 ワード

問題は最高の指導者で、細心の注意は最高の品質です.DCLは必ずvolatileを追加しなければなりませんか?

一、DCL

DCL全称double check lock、二重チェックロックこれは単例モードの実現であり、現在の標準装備実現でもあり、その前に / などの実現があり、まずDCLの実現を見てみましょう
public class DclSingleBean {

    private volatile static DclSingleBean instance;

    private DclSingleBean() {
        System.out.println("doSomeThing...");
    }

    public static DclSingleBean getInstance() {
        if (null == instance) {
            synchronized (DclSingleBean.class) {
                if (null == instance) {
                    instance = new DclSingleBean();
                }
            }
        }
        return instance;
    }
}

これは安全な怠け者式の単例実現であり、古典的な設計モデルである.
  • volatileキーワードここでの意味は何ですか?
  • volatileは削除できますか?

  • 二、volatileについて


    前の文章ではvolatileの可視性について話しましたが、もう一つのまぶしい特性があります. と呼ばれています.

    0.JMM-Javaメモリモデル


    Javaメモリモデルでは3つの特性が規定されています
  • 原子性は、付与操作i = 1
  • のようないくつかの操作が原子性を有することを規定する.
  • 秩序性、実行順序の最適化による実行効率の向上
  • の可視性は、1つのスレッドが共有変数の値を変更すると、他のスレッドが直ちにこの変更を知ることができるかどうかである.

  • 以下はJavaメモリモデルと命令の並べ替えから参照します.
  • Happen-Before先行発生規則
  • sychronizedvolatileだけでプログラム実行中の原子性、秩序性、可視性を保証すると、コードは異常に煩雑になる.JMMは、Happen-Beforeのルールを提供し、データ間の競合の有無、スレッド環境の安全性を制約します.具体的には、次のとおりです.
  • 順序原則1つのスレッド内で意味のシリアル性を保証する.a = 1; b = a + 1;
  • volatileルールvolatile変数の書き込みは、まず読み取りに発生し、これはvolatile変数の可視性を保証し、
  • .
  • のロック規則解除(unlock)は、後続のロック解除(lock)の前に必ず発生する.
  • 伝達性AはBより先に、BはCより先に、Aは必然的にC.
  • より先になる.
  • スレッドが起動する、中断する、スレッドを終了するstart()方法は、その各動作よりも先に行われる.
  • スレッドの割り込み(interrupt())は、割り込むスレッドのコードよりも先である.スレッドのすべての動作は、スレッドの終了よりも先に(Thread.join()).
  • オブジェクトの終端オブジェクトのコンストラクタ実行がfinalize()より先に終了する方法.

  • 1.命令再配置とは


    3行のコードがあります
    1  int a = 1;
    2  int b = 2;
    3  int c= a + b;
    

    通常の実行方法は順番に実行されますが、1行目と2行目には前後依存関係がないことがわかります.1行目と2行目を同時に実行すれば、実行速度を向上させることができますか?
    そこでJVMの建設者は同様の最適化を行い、この最適化は であり、その名の通り命令の実行順序を一定の規則に従って並べ替え、より速く動作させる.JVMでこのことをしたのはJIT<Just In Time Compiler>インスタントコンパイラです
    一部の商用仮想マシンでは、Javaプログラムは最初に解釈器(Interpreter)によって解釈実行され、仮想マシンがある方法やコードブロックの実行が特に頻繁であることを発見すると、これらのコードは と認定される.ホットスポットコードの実行効率を向上させるために、実行時にインスタントコンパイラ(Just In Time Compiler)は、これらのコードをローカルプラットフォームに関連するマシンコードにコンパイルし、様々な階層の最適化を行う.
    リファレンスhttps://www.cnblogs.com/linghu-java/p/8589843.html
    したがって、上記コードの実行時には、第1行と第2行が同時に実行する、第3行が実行されるように最適化することができる.

    2.指令再配置による問題


    多くの場合の命令再配置は運転効率を速めるが、いくつかの特殊な場合の命令再配置はいくつかの位置決めが困難な問題をもたらす可能性があり、そのうちDCLは比較的典型的な1つである.DclSingleBeanクラスのインスタンス化プロセスの解析を容易にするために、フィールドを追加します.
    public class DclSingleBean {
    
        private int x;
        
        public int getX() {
            return x;
        }
        
        private volatile static DclSingleBean instance;
    
        private DclSingleBean() {
            x = 8;
            System.out.println("doSomeThing...");
        }
    
        public static DclSingleBean getInstance() {
            if (null == instance) {
                synchronized (DclSingleBean.class) {
                    if (null == instance) {
                        instance = new DclSingleBean();
                    }
                }
            }
            return instance;
        }
    }
    
  • インスタンス変数xを増加する、構築時に初期値
  • を付与する.DclSingleBeanのクラスのインスタンス化プロセスは、以下のステップに簡略化することができる.
    private volatile static DclSingleBean instance;
    private DclSingleBean() {
        x = 8;
    }
    instance = new DclSingleBean();
    
  • メモリ領域
  • は、スタック内でinstanceに割り当てる.
  • オブジェクトフィールドには の値が付与、ここでのxintのタイプであるため、このときx0
  • である.
  • は構造方法を呼び出し、初期値x8
  • とする.
  • スタック内参照をスタック内オブジェクトと接続する
  • オブジェクト作成完了
  • 並べ替えが許可されている場合、つまりvolatileのキーワードを削除し、第3ステップ構造と第4ステップ確立変換の実行順序が入れ替わるとしたらどうなりますか?
  • スタック内参照をスタック内オブジェクトと接続する
  • は構造方法を呼び出し、初期値x8
  • とする.
    接続を確立する後、他のスレッドがこのとき第1の重みのnull == instanceに入ると判断し、falseが得られたのは、接続が確立するからである.そしてこの の対象instanceに直接戻る、そのうちのxの値が0となると、ここでの問題が露呈する.
    説明
  • 命令再配列が発生する確率は非常に小さく、
  • が発生する可能性があるのは一定のレベルの同時性が必要である.
    これはvolatileの禁止命令の並べ替えの経典の例で、よく考えてみることができて、陥りやすい誤区は私がロックをかけた以上、どうして他のスレッドが現在のスレッドを手に入れることができてまだ実例の完全な対象がありませんか?
  • ロックされたコードブロックは、第1の再判定と第2の再判定との間で、すべてのスレッドが第1の再判定
  • に入る.
  • static修飾変数はグローバルに可視であり、接続が確立されるとnullではないに違いない
  • .

    3.指令再配置禁止-メモリバリア


    メモリバリアは命令の間の高い壁であり、越えてはいけないことを理解しています.読み書きだけが順番に分かれていて、それぞれ以下の命令です
  • loadホストメモリからワークメモリ
  • にデータを読み出す.
  • storeワークメモリからプライマリメモリ
  • にデータを書き込む.
    それに対応して4種類のメモリバリアがあります
  • StoreStoreBarrier
  • LoadLoadBarrier
  • StoreLoadBarrier
  • LoadStoreBarrier
  • StoreStoreBarrierは書き込みと書き込みの間のメモリバリアであり、バリアの前のstoreの動作がバリアの後のstoreに発生することを保証する、他の3つは類似している.StoreLoad Barriersは他の3つのバリアの効果を同時に備えているため、全能バリア(mfence)とも呼ばれ、現在多くのプロセッサでサポートされている.しかし、他のバリアに比べて、バリアのオーバーヘッドは比較的高価である.
    オペレーティングシステムレベルの実装命令を拡張
  • Windows
  • lock addl

  • Linux
  • sfence
  • lfence
  • mfence


  • 詳細はメモリバリアの詳細を参照してください

    4.命令再配置の例


    コードを貼り付けて、ソースMemory Reordering Caught in the ActをJava言語で実現した.
    public class CatchReOrderClass {
    
        private static int a, b, x, y;
    
        public static void main(String[] args){
    
            int i = 0;
            for (;;) {
                a = 0; b = 0;
                x = 0; y = 0;
                new Thread(() -> {
                    a = 1;
                    x = b;
                }).start();
    
                new Thread(() -> {
                    b = 1;
                    y = a;
                }).start();
    
                i++;
                if (x == 0 && y == 0) {
                    System.out.println(i);
                    break;
                }
            }
        }
    }
    

    実行回数が十分に多い場合にはループが終了する、このときコマンドの並び替えが発生したことを示す.