Javaの揮発性の特性の深い理解

まず、揮発性の理解

1.volatileはJava仮想マシンによって提供されます軽量同期メカニズム。

  • 視認性の保証
  • 原子性は保証されません
  • 命令の再配置を無効にする

視認性の保証

可視性とは何ですか?
JMM(Javaメモリモデル)
JMMは、それ自体には存在しない抽象的な概念であり、プログラム内のさまざまな変数のアクセス方法を定義するための一連の仕様を記述します。

  • 視認性
  • 原子性
  • 秩序

プログラムを実行しているJVMのエンティティはスレッドであり、各スレッドが作成されると、JVMはそのための作業メモリ(一部の場所ではスタックスペースと呼ばれます)を作成します。作業メモリは、各スレッドのプライベートデータ領域です。 、およびJavaメモリモデルでは、すべての変数がメインメモリに格納され、メインメモリはすべてのスレッドからアクセスが、変数に対するスレッドの操作(割り当ての読み取りなど)を実行する必要があります。まず、メインメモリから独自のワーキングメモリスペースに変数をコピーし、次に変数を操作します。操作が完了すると、変数はメインメモリに書き戻されます。メインの変数メモリを直接操作することはできません。各スレッドの作業メモリは変数のコピーをメインメモリに格納するため、異なるスレッドは互いの作業メモリにアクセスできず、スレッド間の通信(値の転送)はメインメモリを介して行う必要があります。

JMM可視性図:
ここに画像の説明を挿入

揮発性が可視性を保証するというコード証明:

揮発性のキーワード変更は追加されていません

package com.jess.juc;

import java.util.concurrent.TimeUnit;

class Data {
    
    
    int number = 0;

    public void add(){
    
    
        this.number = 60;
    }
}

public class VolatileDemo{
    
    
    public static void main(String[] args) {
    
    
        // 假如int number = 0; number变量之前没有添加volatile关键字修饰
        Data data = new Data();
        new Thread(()->{
    
    
            System.out.println(Thread.currentThread().getName()+" 线程启动");
            //暂停一下线程 3s
            try {
    
    
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            data.add();
            System.out.println(Thread.currentThread().getName()+" 线程更新:"+data.number);
        },"A").start();

        //main线程
        while (data.number == 0){
    
    
            //等待,知道number不在等于0
        }
        //成功打印说明main线程感知到number值变了,体现了可见性
        System.out.println(Thread.currentThread().getName()+" 线程" + "任务完成");
    }
}

ここに画像の説明を挿入
メインスレッドはタスクの完了を出力しません。これは、数値が変更されたことを認識していないことを示しています。

揮発性キーワード修飾子を追加

class Data {
    
    
   volatile int number = 0;

    public void add(){
    
    
        this.number = 60;
    }
}

ここに画像の説明を挿入

揮発性の可視性の実装

  • アセンブリコード命令を生成するとき、揮発性の変更された共有変数が書き込まれるときに、追加のロックプレフィックス命令があります。
  • プレフィックス付きの命令をロックすると、CPUキャッシュがメモリに書き戻されます
  • CPUのメモリへのキャッシュライトバックは、そのメモリアドレスで他のCPUによってキャッシュされたデータを無効にします
  • 揮発性変数は、各スレッドがキャッシュコヒーレンスプロトコルを介して最新の値を取得することを保証します
  • キャッシュコヒーレンスプロトコルにより、各CPUは、バス上を伝播するデータをスニッフィングすることにより、自身のキャッシュの値が変更されているかどうかを確認します。
  • CPUは、自身のキャッシュラインに対応するメモリアドレスが変更されたことを検出すると、現在のCPUキャッシュラインを無効な状態に設定し、メモリからCPUキャッシュにデータを再度読み取ります。

原子性は保証されません

アトミック性とは何ですか?
つまり、1つの操作または複数の操作のいずれかが実行され、実行プロセスが何らかの要因によって中断されないか、いずれも実行されません。

揮発性キーワード修飾子を追加

package com.jess.juc;

class Data {
    
    
   volatile int number = 0;

    public void sub(){
    
    
        number -= 1;
    }
}

public class VolatileDemo{
    
    
    public static void main(String[] args) {
    
    
       //验证volatile不保证原子性
        Data data = new Data();
        //生成20个线程
        for (int i = 0; i < 20; i++) {
    
    
            new Thread(()->{
    
    
                for (int j = 0; j < 100; j++) {
    
    
                    data.sub();
                }
            },String.valueOf(i)).start();
        }

        //需要等待20个线程都完成sub后,在执行main线程
        //后台默认2个线程,一个是main,一个是gc线程
       while (Thread.activeCount() > 2){
    
    
           Thread.yield();
       }
        System.out.println(Thread.currentThread().getName()+"  number:"+data.number);
    }
}

volatileがアトミック性を保証できる場合、numberの最終値は-2000になります。volatileが
ここに画像の説明を挿入
アトミック性を保証しないのはなぜですか?
numは最初は0であり、スレッド1、2、および3がデータの更新(+1)後にメインメモリに書き戻さなければならない場合があります。CPUスケジューリングにより、スレッド1と2が中断されるため、3つのスレッド(num = 1)の書き込み操作を実行します。このとき、3スレッドで更新されたデータがメインメモリに書き込まれ、1スレッドと2スレッドが再度書き込み、numの値を上書きしても、numの値は1のままです(numの更新通知は受信されていません)。まだ)。アトミック性が保証されている場合、numは3である必要があります。(つまり、書き込み値が失われる場合があります)

(ロードからストアへ)は安全ではなく、他のCPUが途中で値を変更すると、値が失われます。

アトミック性を解決する方法は?

  1. 同期を追加(非推奨)
  2. 以下のコードに示すように
package com.jess.juc;

import java.util.concurrent.atomic.AtomicInteger;

class Data {
    
    
   volatile int number = 0;

    public void sub(){
    
    
        number -= 1;
    }

    AtomicInteger atomicInteger = new AtomicInteger();
    public void subMyAtomic(){
    
    
        atomicInteger.getAndDecrement();
    }
}

public class VolatileDemo{
    
    
    public static void main(String[] args) {
    
    
       //验证volatile不保证原子性
        Data data = new Data();
        //生成20个线程
        for (int i = 0; i < 20; i++) {
    
    
            new Thread(()->{
    
    
                for (int j = 0; j < 100; j++) {
    
    
                    data.subMyAtomic();
                }
            },String.valueOf(i)).start();
        }

        //需要等待20个线程都完成sub后,在执行main线程
        //后台默认2个线程,一个是main,一个是gc线程
       while (Thread.activeCount() > 2){
    
    
           Thread.yield();
       }
        System.out.println(Thread.currentThread().getName()+"  number:"+data.atomicInteger);
    }
}

ここに画像の説明を挿入

命令の再配置を無効にする

コンピュータがプログラムを実行するとき、パフォーマンスを向上させるために、コンパイラとプロセッサはしばしば命令を再配置します。
ここに画像の説明を挿入
簡単に言えば、命令の再配置とは、プログラム命令の実行順序がコードの順序と一致しない可能性があることを意味します。

  • シングルスレッド環境では、プログラムの最終的な実行結果がコードの順次実行の結果と一致することが保証されます。
  • コンパイラとプロセッサは、操作を並べ替えることができます。コンパイラーとプロセッサーは、並べ替え時にデータの依存関係を尊重し、コンパイラーとプロセッサーは、データの依存関係を持つ2つの操作の実行順序を変更しません。
  • マルチスレッド環境では、スレッドは交互に実行されます。コンパイラの最適化と再配置が存在するため、2つのスレッドで使用される変数が一貫しているかどうかは不明であり、結果を予測することはできません。

volatileはどのように命令の再配置を禁止しますか?
メモリバリアは、メモリフェンスとも呼ばれ、次の2つの機能を持つCPU命令です。

  1. 特定の操作の保証された順序
  2. 一部の変数のメモリの可視性を保証します(この機能を使用して揮発性メモリの可視性を実現します)
    ここに画像の説明を挿入

おすすめ

転載: blog.csdn.net/myjess/article/details/120294878