Javaはvolatileキーワード(スレッドの原子性、可視性、秩序性)を学習します

このキーワードについて話そうではありません。まず質問させてください。あなたはスレッドを知っています。

可視性

原子性

秩序   

 

これらの3つの特別な機能はありますか?

 

理解できない場合は、以下を読み続けてください

 

Javaの各スレッドが動作しているとき、それは独自の作業メモリを持ちます。この作業メモリは、スレッドだけがアクセスし、他のスレッドはアクセスできません。

各スレッドがデータを読み書きするとき、最初にメインメモリ内のデータを独自のワーキングメモリにコピーし、次にスレッドがデータを変更するために読み書きする必要がある場合、最初に独自のワーキングメモリで読み書きします。次に、データをメインメモリにフラッシュします。しかし、それはいつメインメモリにフラッシュされますか?誰にも言えない...

関与する要素が多すぎるため

 

============== [ 可視性 ] ==============

現在、次のような現象があります。

メインメモリにはisExit属性があります。

スレッド1がコピーを独自の作業メモリに読み取り、

スレッド2はまた、変数isExitのデータをその作業メモリーにコピーし、変数isExitの値を変更します。

たとえば、次のコード:

    private boolean isExit = false;

    public void testVolati(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (!isExit) {
                    //do something
                }
            }
        }, "线程1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                isExit = true;
                //do something
            }
        }, "线程2").start();
    }

このisExitがリアルタイム応答を必要としない場合、これは問題ありません。値を変更する必要がある場合、他のスレッドはすぐに認識し、最新の値を更新する必要があるため、問題があります。

何?どうして?

 

スレッド2はisExitの値を変更するため、スレッド2がメインメモリにフラッシュするタイミングは不明です。

スレッド2が値を変更する前に、スレッド1がisExit(つまりisExit = false)を独自の作業メモリにコピーした場合、スレッド1はメインメモリに移動しません

この値を読み取ります。したがって、スレッド1の値は常にisExit = falseです。したがって、スレッド1の実行関数が実行されています。スレッド2はisExitの値を変更しましたが。

 

これは不可視 のスレッドであり、スレッドが他のスレッドの値を変更すると、すぐにはわからない場合があります

 

この問題を解決するために、Javaが提供   するキーワード揮発性が 。限り、変更した属性によって  揮発性が 修正され、すべてのスレッドがすぐにそれについて学びますと、メインメモリでそれを再読み込みし、その後、独自のワーキングメモリにそれを更新します。たとえば、上記のコードを変更して、isExitにvolatile を使用します。  

    private volatile boolean isExit = false;

    public void testVolati(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (!isExit) {
                    //do something
                }
            }
        }, "线程1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                isExit = true;
                //do something
            }
        }, "线程2").start();
    }

これにより、volatile を介して isExitが変更されます。スレッド2がisExitの値を変更すると、スレッド1は即座にそれについて学習し、メインメモリから読み取り、独自の作業メモリでisExitに更新します。したがって、volatileによって   変更され   た変数に    スレッドの可視性があり  ます

============== [ 原子性 ] ==============

さて、アトミック性とは何かを見てみましょう。今のところ話はしません。前のコードを見てみましょう。

    private volatile int num = 0;//累加的变量
    private int count = 0;//完成线程累加
    private int THREAD_COUNT = 20;//线程数目

    public void textVo() {
        //开启20个线程
        for (int x = 0; x < THREAD_COUNT; x++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        num++;
                    }
                    threadOK();
                }
            });
            thread.start();
        }

    }

    private void threadOK() {
        count++;
        //等待所有线程都执行完累加
        if (count == THREAD_COUNT) {
            Log.d(TAG, "num : " + num);
        }
    }

 

コードを見てください。コードは非常にシンプルで、20スレッドを開始します。各スレッドは変数numを10000、論理的には20 * 10000まで累積します。結果は200000になります。

では、出力を見てみましょう。

 

確率は私たちが期待する数より少ないです、何ですか?あなたは私をからかっている????

なぜこんな感じ?さあ、あなたのために分析させてください:

実際、問題はnum ++の行に現れます。Java言語が最終的にアセンブリ言語にコンパイルされ、アセンブリ命令によって操作されることは誰もが知っています。

num ++のコード行も例外ではありません。最終的には、実行のためにN個のアセンブリー命令にコンパイルされ、おそらく逆コンパイルされたアセンブリー命令になります。

 

		Code:
		Stack=2,Locals=0,Args_size=0,
		0:	getstatic  #98;//
		3:	iconsts_1
		4:  iadd
		5:  putstatic  #98
		8:  return

 

プロセス全体は、おそらく組み立て説明書にあります:

 

1:最初にデータを読み取り、この値を操作スタックの最上部に配置します(行0)

2:データを蓄積する(4行目)

3:蓄積されたデータを返す(8行目)

 

 

次に、この時点で問題が発生します。

といった:

1:私のスレッド1は100に等しいnumの値を読み取り、この値を操作スタックの最上位に置きます。このとき、CPUの実行能力は他のスレッドによって奪われます

2:スレッド2は、この時点でCPUの実行権を取得し、numの値を100として読み取り、累積後に101をnumに返します。このとき、num = 101

3:次に、今度はスレッド1によってCPU実行権が奪われます。スレッド1のスタックの最上位の値は100のままです。その後、値クラスが追加されて101が取得され、値がnumに返されます。

4:その後、問題は非常に明確です。スレッド1とスレッド2は1回累積され、最終的な値は1だけ増加します。

 

しかし、numが  volatile     によって変更され、2行目が変更された後、なぜスレッド1がすぐに認識せず、メインメモリに移動して新しいデータを読み取らないのでしょうか。スレッドの可視性があるということではありませんか?

 

この質問をするのは良いことですが、アセンブリ命令が値の読み取りを演算スタックの最上位に置き、他のスレッドで変数numをどのように操作するかは関係がないことを伝えるのは残酷です。後続の累積演算命令は、元のスタックの最上位にあるすべてのデータです。

 

これスレッドの  原子性です 

 

原子性、つまり仮想マシンがJava命令を実行する場合 、別のスレッドCPUがすぐに実行する前に、スレッド    がJava命令 を完了する必要  があることを保証できます。これはJava命令あることに注意してください 

たとえば、num ++のようなJavaコードの単純な行、

明らかに、volatile    キーワードで変更された変数は使用できません。つまり、num変数にはスレッドのアトミック性がありません。

num ++も含まれているため、このような単純な自動インクリメント命令は、実行される前に他のスレッドによって実行されることを保証しません。

実際、別の見方をすると、スレッドの計算機能は原子性を保証できないと考えることができます。

 

============== [ Orderliness ] ==============

さて、最後に、秩序について話します。

Java仮想マシンの実行命令の動作についてお話ししましょう。最後のデモについてお話しましょう

    int a = 0;//第1行
    int b = 1;//第2行
    int c = 2;//第3行
    private void test() {
        a++;      //第4行
        b++;      //第5行
        c = a + b;//第6行
    }

ほんの数行のコードです。仮想マシンは保証されません

4行目は必ず5行目の前に実行されます。仮想マシンがコンパイルされると、Javaが再最適化およびソートされます。

保証のみ:実行がコードの6行目に達したとき、4行目と5行目が実行されている必要があります。

つまり、最終結果の出力が正しいことのみを保証します。実行順序がユーザーが書いたものと一致する保証はありません。

さて、揮発性の 変更なしでDoubleLockシングルトンモードがどうなるかみましょう 

 

さて、volatile なしの最後の  シングルトンモード、標準のDoubleLockシングルトンモード、

public class ThreadDemo {

    private static ThreadDemo threadDemo;

    public ThreadDemo getInstance() {
        if (threadDemo == null) {
            synchronized (ThreadDemo.this) {
                if (threadDemo == null) {
                    threadDemo = new ThreadDemo();//第9行代码
                }
            }
        }
        return threadDemo;
    }

}

volatileを 追加しない と、スレッドセーフティの問題が発生します。追加しないでください。

実際、スレッドセーフティはコードの9行目に表示されますが、実際には、コードの9行は3つのステップに分割されており、デフォルトで実行されます。

1:ヒープメモリ内のメモリ空間をThreadDemoのインスタンスに割り当てます(つまり、新しいThreadDemo())。

2:メモリインスタンスを初期化します( ThreadDemoコンストラクターを呼び出してインスタンス初期化します)

3:内部メモリにthreadDemo変数を作成し、手順1で作成したアドレスをポイントします(この手順が実行されている限り、threadDemoは空ではありません

 

明らかに、threadDemo = new ThreadDemo()の行は   3つのステップに分かれています。

さらに重要なことは、今言ったように、デフォルトの順序であっても、コンパイラーはコードの実行順序を最適化することがあります。

コードを最適化した後の順序は、1-2-3  または次のいずれかです。1-3-2

1-2-3はそれで大丈夫でしょう

 

コンパイラが実行順序を1-3-2に変更した場合にどのような問題が発生するかを見てみましょう

シーンを作成します。

スレッド1が最初に来て、最初にコードの9行目を実行します。1〜3を実行した後、threadDemoは既に空ではありません。このとき、CPU実行能力はスレッド2によって奪われます

このとき、スレッド2はCPUの実行権を取得し、シングルトンを取得するためにやって来て、threadDemoが空でないと判断し、直接取り除いて使用します。

 

これは問題です。現時点では:threadDemoは単にメモリアドレスを指していますが、このアドレスに格納されているデータはまだ初期化されていません。この例では、nullポインターなど、予期しない問題が多数発生します。

 

したがって、この問題を解決するために、Javaはキーワードvolatileを提供し   ます。このキーワードで変更された変数はコンパイラーに伝えます。

この変数の前のコードは、最初にコンパイラーによって実行される必要があります。

この変数の後のコードは、コンパイラーが保証した後に実行する必要があります。

発音しにくい?わかりにくいですか?

数行のコードを書きます。

volatile int a = 1;
int b = 1;
int c = 1;
int b = 1;

private void test(){
    b++;//第7行
    c++;//第8行

    a++;//第9行

    b = c+b;//第11行
    c = b+;10//第12行

}

 

変数aは  volatile  で変更され、コンパイラは

7行目と8行目のコードは、9行目より前に実行する必要があります

11行目と12行目のコードは、9行目以降に実行する必要があります

ただし、7行目と8行目で最初に実行される行は保証されていません。

もちろん、11行目と12行目のどちらの行が最初に実行されるかは保証されません。

 

変更された揮発性  変数は分割線に相当します。前述のコードはそれ自体の前に実行し、次のコードはその後に実行する必要があります。

 

さて、ちょうど今、単一の例のモードに戻りましょう、我々は場合。threadDemo変数を変更して  揮発性  、我々は、コンパイル後の順番でなければなりませんので、手順1と2は、ステップ3の前に実行されなければならないことを確実にすることができます:1-2-3、したがって、threadDemoがメモリアドレスをポイントした後、ヒープメモリアドレスのデータは初期化されている必要があります。したがって、スレッドセーフの問題はありません。

 

したがって、正しいDoubleLockシングルトンモードは次のようになります。

public class ThreadDemo {

    private volatile static ThreadDemo threadDemo;

    public ThreadDemo getInstance() {
        if (threadDemo == null) {
            synchronized (ThreadDemo.this) {
                if (threadDemo == null) {
                    threadDemo = new ThreadDemo();//第9行代码
                }
            }
        }
        return threadDemo;
    }

}

 

要約すると:

変更された変数と  揮発性  

スレッドの原子性なし

    スレッドの可視性

    スレッド順

 

 

上記のコードに問題はありません。修正のためにメッセージを残してください。ありがとうございました

 

 

 

 

 

 

 

 

 

 

 

 

 

おすすめ

転載: blog.csdn.net/Leo_Liang_jie/article/details/91465477