あなたのJavaコンカレントプログラムBugは、100%このいくつかの原因で発生しています.


可視性の問題
可視性とは、1つのスレッドが共有変数を修正し、他のスレッドがその共有変数の更新後の値をすぐに見ることができることを意味します.これは合理的な要求と見なされますが、マルチスレッドの場合、失望するかもしれません.CPUごとに独自のキャッシュがあるため、スレッドごとに異なるCPUが使用されている可能性があります.データの可視性に問題が発生します.まず、次の図を見てみましょう.
共有変数countについては、各CPUキャッシュにcountコピーが1つあり、各スレッドの共有変数countに対する操作は、自分が存在するCPUキャッシュのコピーのみを操作し、ホストメモリまたは他のCPUキャッシュのコピーを直接操作することはできず、データ差が生じる.可視性がマルチスレッドの場合に発生するプログラムの問題の典型的な例は、次のような変数の累積です.
public class Demo {

    private int count = 0;

    //      count + 10000
    public void add() {
        for (int i = 0; i < 10000; i++) {
            count += 1;
        }
    }

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 10; i++) {
            Demo demo = new Demo();
            Thread t1 = new Thread(() -> {
                demo.add();
            });
            Thread t2 = new Thread(() -> {
                demo.add();
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(demo.count);
        }
    }
}

2つのプログラムを使用してcount変数を加算し、スレッドごとに10000回加算し、理屈では最終結果は20000回であるはずですが、何度も実行すると、結果は20000回とは限らないことがわかります.これは共有変数の可視性によるものです.
2つのスレッドt 1,t 2を起動すると、スレッド起動時に現在のメインメモリのcountが自分のCPUキャッシュに読み込まれ、このときcountの値が0であっても1であってもよいし、デフォルトでは0であり、各スレッドはcount+=1操作を実行する.これは並列操作であり、CPU 1とCPU 2のキャッシュのcountはいずれも1である.そして、彼らはそれぞれ自分のキャッシュのcountをメインメモリに書き戻します.このとき、メインメモリのcountも1で、私たちが予想した2ではありません.この原因はデータの可視性によるものです.
げんしせいもんだい
原子性:1つの操作または複数の操作は、すべて実行され、実行されたプロセスがいかなる要因によっても中断されないか、実行されないかのいずれかです.この原子性はCPUレベルのもので、私たちのJavaコードの中の原子性ではありません.私たちの可視性Demoプログラムのcount += 1;コマンドを例にとると、このJavaコマンドは最終的に次の3つのCPUコマンドにコンパイルされます.
  • 変数countをメモリからCPUのレジスタにロードし,count=1
  • とする.
  • レジスタでcount+1動作を実行し、count=1+1=2
  • 結果+1後のcountをメモリ
  • に書き込む
    これは典型的な読み取り-変更-書き込みの操作ですが、それは原子的ではありません.マルチコアCPUの間には競争関係があり、あるCPUがずっと実行しているわけではありません.彼らは絶えず実行権を奪い、実行権を解放するので、上の3つの命令は必ずしも原子的ではありません.次の図は2つのスレッドcount += 1命令のシミュレーションの流れです.
    スレッド1が存在するCPUは、最初の2つの命令を実行した後、実行権がスレッド2が存在するCPUによって奪われると、スレッド1が存在するCPUは保留して再び実行権を取得し、スレッド2が存在するCPUは実行権を取得した後、まずメモリからcountを読み出し、このときメモリのcountはやはり1であり、スレッド2が存在するCPUはちょうどこの3つの命令を実行し、スレッド2の実行が完了するとメモリのcountは2に等しくなり、スレッド1が再び実行権を取得すると、スレッド1には最後の count のコマンドしか残っていません.実行が完了すると、メモリのcountの値は2で、私たちが予想した3ではありません.
    秩序性の問題
    秩序性:プログラムの実行順序はコードの前後順に実行されます.例えば、次のコードです.
    1  int i = 1;
    2  int m = 11;
    3  long x = 23L;

    秩序性によってはコード順に実行する必要がありますが、実行結果は必ずしもこの順ではありません.JVMはプログラムの実行効率を高めるために、上のコードに対してJVMコンパイラが最適とする順に実行するため、コードの実行順序を乱す可能性があります.プログラムの最終的な実行結果とコード順序の実行結果が一致することを保証します.これは私たちが言った命令の再ソートです.
    命令の並べ替えによってプログラムがBugを出す典型的な例は、volatileキーワードを付けない二重検出ロック単例モードであり、以下のコードである.
    public class Singleton { 
        static Singleton instance; 
        public static Singleton getInstance(){ 
        //      
        if (instance == null) { 
            //   ,           
            synchronized(Singleton.class) { 
                //      
                if (instance == null) 
                    //     ,          
                    instance = new Singleton(); 
                } 
        }
        return instance; 
        } 
    }

    二重検出ロック方式は完璧に見えますが、実際に実行するとBugが発生し、オブジェクトが逸脱する問題が発生し、構築されていないSingletonオブジェクトが得られる可能性があります.これはSingletonオブジェクトを構築するときに再ソートを指令する問題です.まず、コンストラクションオブジェクトの理想的な操作命令を見てみましょう.
  • 命令1:メモリMを割り当てる;
  • 命令2:メモリM上でSingletonオブジェクトを初期化する;
  • 命令3:Mのアドレスはinstance変数に割り当てられる.

  • しかし、実際にはJVMコンパイラではそうではない可能性があります.次のように最適化される可能性があります.
  • 命令1:メモリMを割り当てる;
  • 命令2:Mのアドレスをinstance変数に割り当てる;
  • 命令3:最後にメモリM上でSingletonオブジェクトを初期化する.

  • 小さな最適化、つまりこのような小さな最適化はあなたのプログラムを安全にしないように見えます.ロックを奪ったスレッドが命令2を実行した後、このときのinstanceはもう空ではありません.このときスレッドCが来て、スレッドCが見たinstanceはもう空ではありません.instanceオブジェクトに戻ります.このときのinstanceは初期化に成功せず、instanceオブジェクトのメソッドまたはメンバー変数を呼び出すと空のポインタ異常がトリガーされる可能性があります.実行可能なフローチャート:
    以上がJavaプログラムがマルチスレッドの場合にBugを出す3つの原因であり,これらの問題についてJDK社も対応する解決策を示しており,具体的には下図に示すように,これらの解決策の詳細は後述する.
    文章の足りないところは,皆さんが多くの点を指摘し,共に学び,共に進歩することを望んでいる.
    最後に
    小さな広告を打って、コードをスキャンして微信の公衆番号に注目することを歓迎します:“平頭兄の技術の博文”、一緒に進歩しましょう.