【Java勉強ノート・並行プログラミング】キーワード volatile 詳細解説

序文

この記事では、Java 並行プログラミングのキーワード volatile とそれに関連する原則、および関連する基本的なコンピューターの知識について紹介します。

1. プロジェクトの背景

最近のプロジェクトでは、マルチスレッドのものがたくさん書かれています。その中には、揮発性キーワードの適用につながるマルチタスクライトブロッカーの実装があり、ところで原理と関連する基礎知識を学びます。

マルチタスクは複数のスレッドで実行されるため、マルチタスクのブロックは当然スレッド。そのため、スレッド内のいくつかの変数を表示する

スレッドの可視性に関しては、Java には volatile と synchronize の 2 つの実装方法があります。

volatile は、すべてのスレッドに対する変数の可視性を確保するためにのみ使用されますが、原子性を保証するものではありません

ただし、同期のロックが重すぎるため、ロックを取得した 1 つのスレッドのみがタスクを実行できるため、このインタラプタを実装するために volatile を選択します。

第二に、揮発性の実装の原則

まず、スレッド間でスレッドが見えない理由を、コンピューターの基になるメモリ モデルから確認してみましょう。

1. スレッドが見えないのはなぜですか?

コンピュータメモリモデル

現在の CPU の動作速度は、メモリの読み書き速度よりもはるかに高速であるため、CPU が十分に計算できるようにするために、実際のアプリケーション条件と組み合わせて、読み書き速度の異なるメディアを使用して、コンピュータ。このうち、CPUは最上位の4層キャッシュ構造(内蔵3層)で、キャッシュ(Cache)はメモリとプロセッサ間のバッファとして使用され、メインメモリとハードディスクは外部ストレージです。メディア。

ここに画像の説明を挿入
CPU の計算はすべて独自の高速キャッシュで実行され、メイン メモリとの読み書きの回数を減らして動作効率を確保します。次の図に、特定のメモリ モデルを示します。

ここに画像の説明を挿入
上のモデルに示すように、コンピュータは必要なデータをメイン メモリからキャッシュにコピーし、計算結果をメイン メモリに書き戻します。計算を実行するプロセスでは、さまざまな非パブリック変数の変更が各スレッドに表示されないため、スレッドのセキュリティが低下します。

JMM メモリ モデル

Java のメモリ モデルには、実際には同じ問題があります。

  • すべてのインスタンス変数とクラス変数はメイン メモリに格納されます。
    (ローカル変数はスレッド プライベートであるため、ローカル変数は含まれないため、競合の問題はありません。)
  • 変数に対するスレッドの読み取りおよび書き込み操作はすべてワーキング メモリで完了しますが、メイン メモリ内の変数を直接読み書きすることはできません。

(写真)
したがって、Java のメモリ モデルでは、スレッドの可視性はまだ解決されていません。では、スレッドの可視性の問題を解決するにはどうすればよいでしょうか。

2.可視性を解決するには?

コンピューターと JMM のメモリ モデルによると、スレッド間で変数を表示するには、次の 2 つの方法しかないようです。

  • 変数をロックすると、ロックを取得したスレッドが変数を計算でき、他のスレッドはブロックされます。(悲観ロック:同期)
  • 書き込み変数はすべてメイン メモリに書き込まれます。メイン メモリ変数が更新されると、変数の有効期限が切れたことが他のスレッドに通知され、更新された変数が他のスレッドの後続の計算で使用されます。(揮発性)

ロックを取得したスレッドのみが計算を実行できるため、同期ペシミスティック ロックを使用すると、スレッド間の変数の可視性原子性が確実に達成されますしかし、コストが高く、同時実行性が低くなります。

volatile の使用はその原子性を保証できませんが (スレッドは安全ではありません)、スレッド間の可視性は保証できます. したがって、マルチタスク ブロッカーとして、volatile キーワードの適用はプロジェクトの要件を満たします。

volatile の範囲とそのアプリケーションのリスクをよりよく理解するために、最初に volatile のメモリ セマンティクスを確認できます。

volatile のメモリ セマンティクス

メモリ セマンティクスは、volatile が計算を実行するときにメモリに実装される関数とルールとして理解できます。

  • volatile 変数を書き込むと、JMM はスレッドに対応するローカル メモリ内の共有変数の値をメイン メモリに更新します。
  • 揮発性変数を読み取る場合、JMM はスレッドに対応するローカル メモリを無効にし、共有変数をメイン メモリから読み取ります。

上記のメモリセマンティクスを実現するには、JMM の命令再配置を制限する必要があります。

3. メモリバリア制御下での命令再配置

コマンドの再配置

まず、命令の再配置とは何かを簡単に紹介します。

パフォーマンスを最適化するために、コンパイラは as-if-serial を前提として特定の規則に従って並べ替えます (どのように並べ替えても、シングル スレッドでの実行結果は変更できません)。

命令の再配置を禁止する意味は次のとおりです。

volatile 変数を初めて書き込むときは、更新された変数をメイン メモリに更新します。スレッドが volatile 変数を読み取るときは、最初にメイン メモリ内の変数を読み取る必要があります。これにより、正しく機能することが保証されます。

命令の再配置によって引き起こされる問題の典型的な例は、シングルトン モードのダブル チェックです. ここでは最初に疑似コードと簡単な例を示します:

volatile boolean testMark = false;
int a = 2
//线程1执行task1
task1() {
    
    
	while(!flag) {
    
    
	
	}
	dosomething(a);
}
//线程2执行task2
task2() {
    
    
	a = 3;
	flag = true;
}

上記の例では、コーダーが値 a = 3 を dosomething 関数に入力することを意図していたことは明らかです。ただし、task2 で 2 つの実行ステップを並べ替えると (単一のスレッドの場合、a = 3、flag = true の順序が逆になっても、このスレッドの最終的な計算結果は変更されないため、行を並べ替える)。

その後、命令を並べ替えると、実行後の関数が正しくない可能性があることが明らかなため、volatile キーワードによって変更された変数の前後では、命令の並べ替えが禁止されます。

命令の並べ替えを防ぐ方法は、メモリ バリアを使用することです。ここでは紹介しませんが、定義を見てください。

Java コンパイラは、一連の命令を生成するときに、適切な位置にメモリ バリア命令を挿入して、特定の種類のプロセッサの並べ替えを禁止します。

起こる - 前に

発生 - before には 2 つの定義があり、8 つのルールで表されます。これらはすべて JMM で実装されているため、あまり心配する必要はありません。ここでは、volatile のルールを確認するだけです。

揮発性ドメインの規則: 揮発性ドメインへの書き込み操作は、任意のスレッドによる揮発性ドメインの後続の読み取りの前に発生します。

上記の例のように、flag = true に変更した後、次回は task1 関数の while ループで flag の値が true であることを確認する必要があります。

4. volatile は原子性を保証しますか?

volatile は、long および double の代入をアトミックにすることができます。(これは別の日に展開されます。)

しかし、volatile は計算. 実際、最も古典的な操作は ++ です。

変数 ++ 操作がある場合、次の 3 つのステップが 1 つのスレッドで生成されます。

  • メインメモリから作業メモリに値をフェッチする
  • ワーキングメモリの値を計算する
  • 計算された新しい値をメイン メモリに更新します

対応する CPU 命令は次のとおりです。

mov    0xc(%r10),%r8d ; Load
inc    %r8d           ; Increment
mov    %r8d,0xc(%r10) ; Store
lock addl $0x0,(%rsp) ; StoreLoad Barrier

同じ volatile 変数の ++ 操作を実行する 2 つのスレッドがある場合、考えられる状況は次のとおりです。

  • スレッド 1 が変数をワーキング メモリに読み取った後、ロックがないため、スレッド 1 が変数をコピーした後、スレッド 2 が CPU を取得し、すぐにメイン メモリ内の変数をワーキング メモリにコピーします。
  • その後、スレッド 1 とスレッド 2 が連続して作業メモリをインクリメントしました。
  • 最後に、スレッド 1 と 2 はそれぞれ、自己インクリメントされた値をメイン メモリにフラッシュします。

前に言ったことに注意してください: 「メインメモリに書き戻すと、他のスレッド変数が無効になります」は、まだ実行されていない上記の例では、スレッド 1 の計算後、スレッド 2 の計算がすぐに実行され (スレッド 1 はこの時点で書き戻されていません)、更新された値が書き戻されると仮定すると、スレッド 2 はその値を無効にしません。変数 (計算が完了したため) であるため、スレッド 2 が書き戻すと、スレッド 1 によって書き戻された値が上書きされ、スレッドが安全でなくなるため、揮発性の計算は原子性を保証できません

3. 揮発性と同期性

このセクションでは、揮発性と同期を組み合わせたアプリケーション、つまりシングルトン モードのダブル チェックについて説明します。

1.シングルトンのダブルチェック

プログラムがシングルスレッドの場合、シングルトン パターンはまったく心配する必要はありません。ただし、マルチスレッドの場合でも、スレッドのセキュリティの問題を確認する必要があります。

最初に単純なシングルトンを作成します。

public class Singleton {
    
    

    private static Singleton singleton = new Singleton();

    private void Singleton(){
    
    }

    public static Singleton getSingleton(){
    
    
    
        return singleton;
        
    }
}

明らかに、上記の空腹スタイルの書き込み。インスタンスは JVM の開始時に作成され、インスタンスの作成時にスレッドのセキュリティ上の問題は発生しません。(でも可能)

しかし、怠惰なスタイルで書かれていると、明らかにスレッド セーフの問題が発生します。

public class Singleton {
    
    

private static Singleton singleton = null;

    private void Singleton(){
    
    }

    public static Singleton getSingleton(){
    
    

        if(singleton == null){
    
    

            singleton = new Singleton();

        }

        return singleton;

    }

}

インスタンスを取得する必要があるスレッドが 2 つある場合、インスタンスの作成時に明らかにシングルトン モードではなく、スレッドが安全でないことは容易にわかります。

また、get 関数の前にロックを追加することもできます。

    public static synchronized Singleton getSingleton(){
    
    

        if(singleton == null){
    
    

            singleton = new Singleton();

        }

        return singleton;

    }

しかし、これはパフォーマンスの急激な低下を意味します。これは、インスタンスを取得するときに、ロックを取得するスレッドが常に 1 つしか機能しないためです。

ただし、少し改善する場合は、次の方法でロックします。

public class Singleton {
    
    

    private static Singleton singleton =null;

    private void Singleton(){
    
    }

    public static Singleton getSingleton(){
    
    
        if(singleton == null){
    
                                       
            synchronized (Singleton.class){
    
              
                if(singleton == null){
    
            
                    singleton = new Singleton(); 
                }
            }
        }
        return singleton;
    }
}

これによりパフォーマンスは向上しますが、それでもスレッドの不安定性につながります。ここはどうして安全ではないのですか?限定的なケースを想定することができます - オブジェクトを作成する命令が再配置されます:

オブジェクトを作成する 3 つの手順:

  • メモリ空間を割り当てます。
  • コンストラクターを呼び出して、インスタンスを初期化します。
  • 参考までに返送先
  • スレッド 1 はオブジェクト メモリ空間に適用され、メモリ アドレスをメイン メモリに書き戻しますが、オブジェクトはまだ初期化されていません。(ステップ 3 をステップ 2 の前に昇格)

シングル スレッドの場合、最後のシングル スレッドの計算結果は再配置前の計算結果と同じであるため、これは不正ではありません。

  • スレッド 2 は、この時点でインスタンスを取得したいと考え、メイン メモリにアクセスしてオブジェクトのアドレスを取得し、アクセスしたところ、null ポインターであることがわかりました。

したがって、命令の再配置は問題を引き起こします。変数に volatile を追加して、命令の再配置を禁止する。

public class Singleton {
    
    

    private static volatile Singleton singleton =null;

    private void Singleton(){
    
    }

    public static Singleton getSingleton(){
    
    
        if(singleton==null){
    
    
            synchronized (Singleton.class){
    
    
                if(singleton==null){
    
    
                    singleton =new Singleton();
                    }
            }
        }
    return singleton;
    }
}

そのため、変数の前に volatile 修飾子が付けられます。

揮発性と静的の 4 つ

静的に変更された変数: 複数のインスタンス間で、変数の一意性が保証されます。しかし、可視性とアトミック性の保証はありません。
volatile によって変更された変数: 変数は、複数のインスタンス間で一意ではありません。ただし、スレッドの可視性は保証されますが、原子性は保証されません。

したがって、 static volatile によって変更される変数は、複数のインスタンス間の一意性とスレッド間の可視性です。

V. まとめ

  • 該当するシナリオ: プロパティが複数のスレッドによって共有され、1 つのスレッドがこのプロパティを変更し、他のスレッドが booleanflag などの変更された値をすぐに取得したり、軽量同期を実現するためのトリガーとして使用したりできます。
  • 揮発性変数の読み取りおよび書き込み操作は、ロックフリーで低コストです。これは、原子性と相互排除を提供しないため、synchronized の代わりにはなりません。
  • volatile は属性にのみ適用できます. volatile を使用して属性を変更し、コンパイラがこの属性の命令を並べ替えないようにします.
  • volatile は可視性を実現し、シングルトン ダブル チェック(特別なシナリオ)で命令の並べ替えを禁止することで、安全性を確保します。

スレッドの可視性命令の再配置は禁止されています必ずしもアトミックではありません

おすすめ

転載: blog.csdn.net/weixin_43742184/article/details/113887129