揮発性キーワードの簡単な紹介

面接に行く場合、面接官はあなたに尋ねます、揮発性についてのあなたの理解について話してください?
このブログを読んだ後は、落ち着いて答えられると思います。

Volatileは、java仮想マシンによって提供される軽量の同期メカニズムです。

揮発性の3つの特性:

  • 可視性を確保する
  • 原子性の保証はありません
  • 注文の再配置を禁止する

では、これら3つの特性はどういう意味ですか?

可視性を確保する

最初に概念を紹介します
。JMM(Javaメモリモデル、JMM)メモリモデル、それ自体は抽象的な概念であり、実際には存在しません。一連のルールまたは仕様を記述し、この一連の仕様を通じてプログラム内の変数を定義します(インスタンスフィールド、静的フィールド、および配列オブジェクトを構成する要素を含む)アクセスメソッド、同期に関するJMMの規定:

  1. スレッドのロックを解除する前に、共有変数の値をメモリにフラッシュバックする必要があります
  2. スレッドがロックする前に、メインメモリの最新の値を作業メモリに読み取る必要があります
  3. ロックおよびロック解除するには、ロックを使用します

次に、画像を見て、難しいことではないことを理解してください:
ここに画像の説明を挿入
JVM実行メモリのエンティティはスレッドであり、各スレッドが作成されると、JVMはそのための作業メモリ(スタックスペース)を作成し、作業メモリは各スレッドのプライベート領域です。 Javaメモリモデルでは、すべての変数をメインメモリに格納することが規定されています。メインメモリは、すべてのスレッドがアクセスできる共有メモリ領域です。ただし、変数に対するスレッドの操作(読み取り割り当てなど)は、作業メモリで実行する必要があります。メインメモリから自分の作業メモリスペースにコピーし、変数を操作します。操作が完了したら、変数をメインメモリに書き込みます。メインメモリの変数を直接操作することはできません。各スレッドの作業メモリはメインメモリに保存されます。可変コピーのコピーであるため、異なるスレッドは互いの作業メモリにアクセスできず、スレッド間の通信(値の転送)はメインメモリを介して完了する必要があります。

変数がvolatileキーワードを使用する場合、現在のスレッドがメインメモリからコピーされた値を変更すると(この時点で値は現在のスレッドのプライベートメモリにコピーされます)、メインメモリの他の値によってすぐにコピーできることが保証されますスレッドは知っています。(各スレッドがメインメモリから最新の値を取得できることを確認してください(変数はvolatileによって変更されます))

可視性コードの検証:
最初にリソースクラスが必要です

class Data {
    
    
    int number = 0;
    public void add() {
    
    
        this.number = 60;
    }
}

次に、2つのスレッド、メインスレッド、新しいスレッドが同時にリソースを操作します

/**
 * @author Cocowwy
 * @create 2020-09-09-22:33
 */
public class VolatileDemo {
    
    
    public static void main(String[] args) {
    
    
    	Data data = new Data();
        //线程thread1
        new Thread(() -> {
    
    
            System.out.println(Thread.currentThread().getName() + "\t come in");
            try {
    
    
                Thread.sleep(3000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            data.add();
            System.out.println(Thread.currentThread().getName() + "\t update number");
        }, "thread1").start();
		
		//线程main
        while (data.number == 0) {
    
    
            //当number的值一直为0则死循环
        }

        System.out.println(Thread.currentThread().getName() + "\t mission is down main get number:" + data.number);
     }
}

結果は次のとおりです。
ここに画像の説明を挿入
次に、上記のプログラムを簡単に分析します。
まず、リソースクラスDateが変数番号を定義し、この値を変更するメソッドを公開します。次に、メイン、メインがスレッド、次に新しいスレッドthread1があります。 、メインスレッドには無限ループがあります。データの値が60になるとスタックし、thread1スレッドがこのデータの値を変更します。ただし、変更後もメインスレッドはwhileループでスタックします。
上記で紹介したJMMメモリモデルと可視性により、メインスレッドによってコピーされたデータは常に0であり、thread1スレッドがこの値を変更した後も変更がないことがわかります。

次に、リソースクラスを変更し、volatileキーワードを追加するだけです。

class Data {
    
    
    volatile int number = 0;
    public void add() {
    
    
        this.number = 60;
    }
}

結果は次のようになります。
ここに画像の説明を挿入
このようにして、numberの値が変更されたために無限ループが解除されたことがわかります。

したがって、メインの物理メモリの値が変更されたことが他のスレッドに通知された場合でも、volatileが可視性を保証できることがわかります。

原子性の保証はありません

まず、原子性とは何かを理解する必要がありますか?

アトミシティとは、不可分性と整合性を意味します。つまり、スレッドが特定のビジネスを行っている場合、スレッドを途中でブロックしたり分割したりすることはできず、統合する必要があります。

同時に成功するか、同時に失敗します。
JMMの導入によると、スレッドAが共有変数xの値を変更したが、メインメモリに書き戻されていない場合、別のスレッドBBBがメインメモリ内の同じ共有変数xで動作する可能性がありますが、この時点ではAAAスレッドです。作業メモリ内の共有変数xは、スレッドBには表示されません。作業メモリとメインメモリ間の同期のこの遅延により、表示の問題が発生します。(つまり、データをフェッチしてメインメモリに繰り返し挿入する操作は、アトミック操作ではありません)

次に、コードを使用して、volatileがアトミック性を保証しないことを証明します。
最初に、自己インクリメントを提供するメソッドを公開するリソースクラス:

class Data {
    
    
    volatile int number = 0;
    public  void  addPlus() {
    
    
        number++;
    }
}

次回ミアン:

public class VolatileDemo {
    
    
    public static void main(String[] args) {
    
    
        Data data = new Data();
        for (int i = 1; i <= 10; i++) {
    
    
            new Thread(() -> {
    
    
                for(int j=0;j<1000;j++){
    
    
                    //上面为验证原子性  下面为使用AtomicInteger来保证原子性的操作
                    data.addPlus();
                }
            }, "Thread" + i).start();
        }

        //需要等待上面20个线程都全部计算完成后,再用main线程取得最终的结果值看是多少

        // 一个是main线程 一个是gc线程
        while (Thread.activeCount()>2){
    
    
            Thread.yield();
        }
        // 两种方法均可
//        try {
    
    
//            Thread.sleep(5000);
//        } catch (InterruptedException e) {
    
    
//            e.printStackTrace();
//        }

        System.out.println(Thread.currentThread().getName()+"线程获取的number值为"+data.number);
    }
}

結果は次のとおりです。
ここに画像の説明を挿入
上記のコードから、10個のスレッドがあり、各スレッドがリソースに対して1000回の自己インクリメント操作を実行し、可視性を確保するためにvolatileが追加されていることがわかりますが、結果は10000未満であり、アトミック性は保証できず、作業メモリとメインメモリの間に同期遅延があります。

では、この原子の可視性の問題をどのように解決するのでしょうか?
最初の解決策:

class Data {
    
    
    volatile int number = 0;
    public synchronized  void addPlus() {
    
    
        number++;
    }
}

ここに画像の説明を挿入

最初の方法は当然、同期を追加してメソッドをロックすることですが、ここでは別の方法を使用できます。

2番目の解決策:JUCで
アトミッククラスAtomicIntegerを使用する

class Data {
    
    
    /**
     * 原子类
     */
    AtomicInteger atomicInteger=new AtomicInteger(0);
    
    /**
     * 原子的自增操作
     */
    public void addAtomicInteger(){
    
    
        atomicInteger.getAndIncrement();
    }
}

APIを見てみましょう:
ここに画像の説明を挿入
ここに画像の説明を挿入
そしてメインを変更します:

public class VolatileDemo {
    
    
    public static void main(String[] args) {
    
    
        Data data = new Data();
        for (int i = 1; i <= 10; i++) {
    
    
            new Thread(() -> {
    
    
                for(int j=0;j<1000;j++){
    
    
                    //上面为验证原子性  下面为使用AtomicInteger来保证原子性的操作
                    data.addAtomicInteger();
                }
            }, "Thread" + i).start();
        }

        //需要等待上面20个线程都全部计算完成后,再用main线程取得最终的结果值看是多少
        // 一个是main线程 一个是gc线程
        while (Thread.activeCount()>2){
    
    
            Thread.yield();
        }
        // 两种方法都行
//        try {
    
    
//            Thread.sleep(5000);
//        } catch (InterruptedException e) {
    
    
//            e.printStackTrace();
//        }

        System.out.println(Thread.currentThread().getName()+"线程获取的atomicInteger值为"+data.atomicInteger);
    }
}

結果は次のとおりです。
ここに画像の説明を挿入
したがって、同期またはAtomicIntegerを使用してアトミック性を保証できます。

注文の再配置

記述されたコードがJVMによって実行されると、パフォーマンスを向上させるために、コンパイラとプロセッサの両方がコンパイルされた命令を並べ替えます。3つのタイプに分けられます:

  1. コンパイラ最適化の再配置:コンパイラの最適化の前提は、シングルスレッドのセマンティクスを変更せずにステートメントの実行順序を再配置することです。
  2. 並列命令の再配置:コード内の特定のステートメント間にデータの依存関係がない場合、プロセッサーはマシン命令に対応するステートメントの順序を変更できます。
  3. プロセッサとメインメモリの間には、2番目と3番目のレベルのキャッシュもあります。これらの読み取り/書き込みキャッシュが存在すると、プログラムのロードとアクセス操作が正常に機能しなくなる可能性があります。

シングルスレッドの条件下では、命令の再配置は最終結果に影響を与えません。つまり、データの整合性を保証できますが、マルチスレッドの条件下では、さまざまなスレッドが交互に実行され、2つのスレッド間で使用される変数は次のようになります。一貫性が保証されている場合、それは保証できません。

次に、シングルトンモードでのvolatileの使用について簡単に紹介します。

シングルトンパターンの一般的な実現には、空腹の人のスタイルと怠惰な人のスタイルがあることは誰もが知っていますが、実際には、シングルトンモードを実現する方法は7〜8つあります。私のこの記事を参照してください:Javaデザインパターンのシングルトンモード

volatileを使用するシングルトンモードは次のとおりです。

DCL(ダブルチェックロック機構)

コードは以下のように表示されます:

/**
 * @author Cocowwy
 * @create 2020-09-09-18:46
 */
public class SingletonDemo {
    
    

    /**
     * 排除指令重排的情况
     */
    private volatile static SingletonDemo instance = null;

    private SingletonDemo() {
    
    
        System.out.println("初始化构造器!");
    }

    /**
     * 双重锁校验 加锁前和加锁后都进行判断
     */
    public static SingletonDemo getInstance() {
    
    
        if (null == instance) {
    
    
            synchronized (SingletonDemo.class) {
    
    
                if (null == instance) {
    
    
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
    
    

        for (int i = 0; i < 10; i++) {
    
    
            new Thread(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    SingletonDemo.getInstance();
                }
            }, i + "").start();
        }
    }
}

結果は次のとおりです。
ここに画像の説明を挿入
コンストラクターが一度だけ初期化されることがわかります。これは、これがスレッドが取得するシングルトンであることを意味します。

おすすめ

転載: blog.csdn.net/Pzzzz_wwy/article/details/108610919