2本の髪を失った、それは揮発性と見なすことができます

本来想着快过年了偷个懒休息下,没想到被兄弟们连续催更,没办法,博主暖男嘛,掐着人中也要更,兄弟们卷起来

volatileキーワードは、Java仮想マシンが提供する最も軽量な同期メカニズムと言えますが、原子性ではなく可視性しか保証できない理由や、命令の再配置を無効にする方法については、まだ十分に理解していない学生が多くいます。

私を信じて、この記事を読み続けると、Javaのコア知識ポイントをしっかりと把握できます

最初にその2つの機能について説明しましょう。

  • スレッドに対するメモリ内の変数の可視性の保証
  • 命令の並べ替えを無効にする

私はすべての言葉を知っています、まとめるとしびれます

これらの2つの関数は通常、Java開発者が正しく完全に理解するのは簡単ではないため、多くの学生はvolatileを正しく使用できません。

視認性について

bbはあまりありません、ここにコードを入れてください

public class VolatileTest {
    
    
    private static volatile int count = 0;

    private static void increase() {
    
    
        count++;
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        for (int i = 0; i < 10; i++) {
    
    
            new Thread(() -> {
    
    
                for (int j = 0; j < 10000; j++) {
    
    
                    increase();
                }
            }).start();
        }
		// 所有线程累加完成后输出
        while (Thread.activeCount() > 2) Thread.yield();
        System.out.println(count);
    }
}

コードは理解しやすいです。同じ共有変数カウントを累積するために10個のスレッドが開かれ、各スレッドは1w回累積されます。

countをvolatileで装飾しました。これにより、メモリ内の10スレッドまでのcountの可視性が保証されます。10スレッドが実行された後、countの値は10wになるはずです。

ただし、何度も実行した後、結果は期待値よりはるかに低くなります
ここに画像の説明を挿入
。どのリンクが問題ですか?
ここに画像の説明を挿入

あなたは文を聞いたことがあるに違いありません:揮発性は可視性を保証するだけで、原子性は保証しません

この文章が答えですが、多くの人はまだ謎を理解していません

長い話なので、短くしておきます。つまり、count ++操作はアトミックではなく、3つのステップで実行されます。

  1. カウントの値をメモリから読み取ります
  2. 実行カウント+1
  3. カウントバックの新しい値を書き戻す

この問題を完全に理解するには、バイトコードから始める必要があります

以下は、increaseメソッドでコンパイルされたバイトコードです。
ここに画像の説明を挿入
理解していなくてもかまいません。1行ずつ見ていきましょう。

  1. GETSTATIC:カウントの現在の値を読み取ります
  2. ICONST_1:スタックの一番上に定数1をロードします
  3. IADD:実装用の+1
  4. PUTSTATIC:カウントの最新の値を書き込みます

ICONST_1とIADDは実際には実際の++操作です

重要な点は、volatileは、GETSTATICステップでスレッドによって取得された値が最新であることを保証することしかできないということですが、スレッドが次の命令行を実行すると、他のスレッドがこの期間中にcountの値を変更する可能性があり、最終的には古い値が実際の新しい値を上書きします

理解して

したがって、並行プログラミングでは、共有変数を変更するためにvolatileのみに依存することは信頼できず、最終的には、スレッドセーフを確保するために主要なメソッドをロックする必要があります。

上記のデモと同様に、少し変更を加えるだけで真のスレッドセーフを実現できます

最も簡単なのは、同期を増加メソッドに追加することです(同期がスレッドセーフを実現する方法については詳しく説明しません。同期の基本的な実装原則については前に説明しました)

    private synchronized static void increase() {
    
    
        ++count;
    }

数回実行してください、
ここに画像の説明を挿入
大丈夫ではありませんか?

これで、次の2つのポイントを新たに理解できるようになります。

  • volatileは、メモリ内の変数がスレッドに表示されることを保証します
  • volatileは可視性のみを保証し、原子性は保証しません

命令再配置について

並行プログラミングでは、実行効率を向上させるために、CPU自体と仮想マシンが命令の再配置を使用します(結果に影響がないことを前提として、一部のコードは順不同で実行されます)

  • cpuについて:cpuを利用するために、命令の実際の実行が最適化されます。
  • 仮想マシンについて:HotSpot vmでは、実行効率を向上させるために、JIT(ジャストインタイムコンパイル)モードでも命令の最適化が実行されます。

命令の再配置は、ほとんどのシナリオで実際に実行効率を向上させることができますが、一部のシナリオはコード実行の順序に強く依存します。この場合、命令の再配置を無効にする必要があります。
ここに画像の説明を挿入
次のシナリオの擬似コードは、「 Java仮想マシン」:

説明されているシナリオは、開発中の一般的な構成読み取りプロセスですが、通常、構成ファイルを処理するときに同時実行は発生しないため、これが問題になるとは認識していませんでした。
初期化された変数を定義するときに揮発性の変更が使用されない場合、命令の並べ替えの最適化により、スレッドAの最後のコード「initialized = true」が事前に実行される可能性があることを想像してください(ここではJavaが疑似コードとして使用されていますが、すべて並べ替えの最適化とは、マシンレベルの最適化操作であり、早期実行とは、このステートメントに対応するアセンブリコードが事前に実行されることを意味します)。そのため、スレッドBの構成情報を使用するコードにエラーが発生する可能性があり、volatileは命令を禁止します。あなたによる再配置はこれが起こるのを避けることができます

命令の再配置を無効にするには、変数を揮発性として宣言するだけで済みますね。

volatileが命令の並べ替えを無効にする方法を見てみましょう

「Java仮想マシンについて」の例も借りてみましょう。
ここに画像の説明を挿入
これはシングルトンモードの実装です。以下はそのバイトコードの一部です。赤いボックスmov%eax、0x150(%esi)はインスタンスへ
ここに画像の説明を挿入
の割り当て割り当て後、lock addl $ 0x0、(%esp)命令も実行されることがわかります。重要なポイントはここにあります。この命令行は、ここでメモリ。メモリバリアの後、cpuまたはvirtual命令が再配置されると、マシンはメモリバリアの後ろの命令をメモリバリアの前に進めることができません。この箇所をよく見てください。


最後に、揮発性についてのすべての人の理解を深めることができる質問を残してください。兄弟、それについて考えてください:

Javaコードは明らかに上から下に実行されますが、なぜ命令の再配置の問題が発生するのですか?

了解しました

おすすめ

転載: blog.csdn.net/qq_33709582/article/details/122415754