Volatileキーワード解読(四)

13174 ワード

volatileの原理


メモリに関する基礎知識を説明しました.このセクションではvolatileの使い方を説明します.
volatileで修飾された共有変数には、次の2つの機能があります.
  • 変数の可視性:変数がスレッドによって変更されると、すぐにメモリに戻り、他のスレッドが
  • で表示されることを保証します.
  • 禁止命令並び替え
  • volatileは可視性を確保できます


    以下は簡単な例です.
    // 1
    boolean flag = false
    while(!flag){
        doSomething();
    }
    
    // 2
    flag = true;
    

    上記のコードはマルチスレッドの場合、デッドサイクルが発生する可能性があります.スレッドAがwhileコードブロックを実行している場合、スレッド2はflagに値付け操作を行いますが、各スレッドには独自のキャッシュ領域があり、スレッド2の操作がメモリに書き込まれていない場合、スレッド1はflagが変化したことを知ることができず、デッドサイクルが行われます.
    volatileキーワードが追加されたらどうなりますか?
     1 
    
     2 , 
    
     2 , , 
    

    volatileは原子性を確保できない


    まず例を見てみましょう
    public class Test {
        public volatile int inc = 0;
    
        public void increase() {
            inc++;
        }
    
        public static void main(String[] args) {
            final Test test = new Test();
            for(int i=0;i<10;i++){
                new Thread(){
                    public void run() {
                        for(int j=0;j<1000;j++)
                            test.increase();
                    };
                }.start();
            }
    
            //  
            while(Thread.activeCount()>1)  
                Thread.yield();
            System.out.println(test.inc);
        }
    }

    この結果は10000だと言われるかもしれませんが、実際には最終的な値が10000未満なのはなぜですか?
    前に原子性について述べたように,自増演算には3段階の操作があり,一致しない可能性が高い.スレッド1がメモリから変数incの値を取得すると仮定すると、スレッドはブロックされ、スレッド2もメモリから変数Incの値を取得する.スレッド1は変数を修正していないため、両スレッドが取得したIncの値はいずれも10であり、このときスレッド2は自己増加演算を行い、メモリに書き込まれ、スレッド1に続いて変数も自己増加演算を行い、最終的には12ではなく11である.
    変数が変更されるとコピーの状態が無効になるのではないかと言う人もいますか?問題は、変数が変更されるとコピーステータスが無効になりますが、スレッド1がメモリから値を取得すると、スレッドはブロックされるだけで、変数のステータスは変更されないため、スレッド2のコピーは無効になりません.
    根源はここにあり,自己増加操作は原子的操作ではなくvolatileも変数に対するいかなる操作も原子的であることを保証できない.
    原子性を保証するために、以下の3つの方法を用いることができる.

    synchronized

    public class Test {
        public  int inc = 0;
    
        public synchronized void increase() {
            inc++;
        }
    
        public static void main(String[] args) {
            final Test test = new Test();
            for(int i=0;i<10;i++){
                new Thread(){
                    public void run() {
                        for(int j=0;j<1000;j++)
                            test.increase();
                    };
                }.start();
            }
    
            while(Thread.activeCount()>1)  // 
                Thread.yield();
            System.out.println(test.inc);
        }
    }

    lock

    public class Test {
        public  int inc = 0;
        Lock lock = new ReentrantLock();
    
        public  void increase() {
            lock.lock();
            try {
                inc++;
            } finally{
                lock.unlock();
            }
        }
    
        public static void main(String[] args) {
            final Test test = new Test();
            for(int i=0;i<10;i++){
                new Thread(){
                    public void run() {
                        for(int j=0;j<1000;j++)
                            test.increase();
                    };
                }.start();
            }
    
            while(Thread.activeCount()>1)  // 
                Thread.yield();
            System.out.println(test.inc);
        }
    }
    

    AtomicInteger

    public class Test {
        public  AtomicInteger inc = new AtomicInteger();
    
        public  void increase() {
            inc.getAndIncrement();
        }
    
        public static void main(String[] args) {
            final Test test = new Test();
            for(int i=0;i<10;i++){
                new Thread(){
                    public void run() {
                        for(int j=0;j<1000;j++)
                            test.increase();
                    };
                }.start();
            }
    
            while(Thread.activeCount()>1)  // 
                Thread.yield();
            System.out.println(test.inc);
        }
    }

    Java 1.5のjava.util.concurrent.atomicパケットの下には、基本データ型の自己増加(プラス1操作)、自己減少(マイナス1操作)、加算操作(プラス1数)、減算操作(マイナス1数)がカプセル化され、これらの操作が原子的操作であることを保証するいくつかの原子操作クラスが提供されている.atomicはCASを利用して原子間操作(Compare And Swap)を実現し、CASは実際にはプロセッサが提供するCMPXCHG命令を利用して実現され、プロセッサがCMPXCHG命令を実行するのは原子間操作である.

    volatileは秩序性を保証する


    volatileキーワード禁止命令の並べ替えには2つの意味があります.
    1)プログラムがvolatile変数の読み取り操作または書き込み操作を実行した場合、その前の操作の変更はすべてすでに行われたことを肯定し、結果はすでに後の操作に対して見える.その後ろの操作はまだ行われていないに違いない.
    2)命令最適化を行う場合,volatile変数にアクセスする文をその後ろに置いて実行したり,volatile変数の後ろにある文をその前に置いて実行したりすることはできない.
    上記のように回りくどいかもしれませんが、簡単な例を挙げます.
    //x、y volatile 
    //flag volatile 
    
    x = 2;        // 1
    y = 0;        // 2
    flag = true;  // 3
    x = 4;         // 4
    y = -1;       // 5

    flag変数がvolatile変数であるため、命令の並べ替えを行う過程で、文3を文1、文2の前に置くことはなく、文3を文4、文5の後ろに置くこともありません.ただし、文1と文2の順序、文4と文5の順序は保証されていないことに注意してください.
    またvolatileキーワードは、文3まで実行すると、文1と文2が必ず実行済みであり、文1と文2の実行結果が文3、文4、文5に表示されることを保証する.
    では、前に挙げた例に戻ります.
    // 1:
    context = loadContext();   // 1
    inited = true;             // 2
    
    // 2:
    while(!inited ){
      sleep()
    }
    doSomethingwithconfig(context);

    文2は文1の前に実行される可能性があり、contextが初期化されていない可能性があり、スレッド2では初期化されていないcontextを使用して操作され、プログラムエラーが発生する可能性があります.
    ここでvolatileキーワードでinited変数を修飾すると、文2に実行するとcontextが初期化されたことが保証されるため、このような問題は発生しません.

    volatileの原理と実現メカニズム


    前述したvolatileキーワードに由来するいくつかの使用について説明したが、volatileが可視性を保証し、命令の再ソートを禁止する方法について検討する.
    次のセクションは、Java仮想マシンを深く理解することから抜粋します.
    「volatileキーワードの追加とvolatileキーワードの追加がない場合に生成されるアセンブリコードを観察すると、volatileキーワードの追加時にlock接頭辞命令が1つ増えることがわかります」
    lock接頭辞命令は実際には1つのメモリバリア(メモリバリアにもなる)に相当し、メモリバリアは3つの機能を提供します.
    1)命令が並べ替えられたとき、その後ろの命令がメモリバリアの前の位置に並べられないことを確保し、前の命令がメモリバリアの後ろに並べられないことを確保する.すなわち、メモリバリアという命令を実行すると、その前の操作がすべて完了する.
    2)キャッシュに対する変更操作を直ちにプライマリ・メモリに書き込むように強制されます.
    3)書き込みの場合、他のCPUで対応するキャッシュ行が無効になります.

    volatileキーワードを使用するシーン


    synchronizedキーワードは、複数のスレッドが同時にコードを実行することを防止するため、プログラムの実行効率に影響しますが、volatileキーワードはsynchronizedよりもパフォーマンスが優れている場合がありますが、volatileキーワードはsynchronizedキーワードに代わることができません.volatileキーワードは操作の原子性を保証できないためです.通常、volatileを使用するには、次の2つの条件が必要です.
    1)変数の書き込み操作は現在の値に依存しない
    2)この変数は他の変数を持つ不変式に含まれていない
    実際には、これらの条件は、volatile変数に書き込むことができるこれらの有効値が、変数の現在の状態を含む任意のプログラムの状態とは独立していることを示している.
    実際、私の理解では、volatileキーワードを使用するプログラムが同時実行時に正しく実行されることを保証するには、上記の2つの条件が原子的な操作であることを保証する必要があります.
    Javaでvolatileを使用するシーンをいくつか挙げます.
  • 状態ビットの識別
  • volatile boolean flag = false;
    
    while(!flag){
        doSomething();
    }
    
    public void setFlag() {
        flag = true;
    
  • double check
  •     /**   */
        private static  volatile Singleton singleInstance;
    
        /**   */
        private Singleton(){};
    
        /**   */
        private static Singleton getSingleInstance(){
    
            /**  ,  */
            if(singleInstance == null){
    
                /**   */
                synchronized (Singleton.class){
    
                    /**  ,  */
                    if(singleInstance == null){
    
                        singleInstance = new Singleton();
                    }
                }
            }
            /**   */
            return singleInstance;
        }

    資料の引用http://www.cnblogs.com/dolphin0520/p/3920373.html