同時およびJava実装(一)-同時設計の三大原則について述べる

5199 ワード

同時設計の三大原則


げんしせい


原子性:共有変数に対する操作は、他のスレッドに比べて干渉できない.すなわち、他のスレッドの実行は、その原子操作が完了した後または開始する前にのみ実行される.
小さな例で理解する
public class Main {

    private static Integer a = 0;

    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(50);
        for (int i = 0; i < 50; i++) {
            pool.submit(() -> {
                a = a + 1;
            });
        }
        pool.shutdown();
        
        // 
        while(!pool.isTerminated());
        System.out.println(a);
    }
}

ここでは、50個のスレッドを含むスレッドプールを作成し、各スレッドに自己増加操作を実行させ、最後にすべてのスレッドの実行が終了した後にaの値を印刷するのを待つ.理論的には、このaの値は50だろうが、実際の運転ではそうではなく、複数回の運転の結果が異なることが分かった.
理由を分析すると、マルチスレッドの場合、a = a + 1という文は複数のスレッドによって同時に実行または交互に実行される可能性がありますが、この文自体は3つのステップに分けられ、aの値、aの値+1を読み出し、aを書き返します.現在aの値が1であると仮定し、スレッドAとスレッドBが実行中である.スレッドAはaの値が1であることを読み出し、aの値+1(スレッドA内のaの値は現在も1)を得、このときスレッドBはaの値が1であることを読み出し、aの値+1をaに書き戻し、このときaは2であり、スレッドAは再び動作し、先ほど+1後のaの値(2)をaに書き戻す.2つのスレッドの実行終了後のaの値は2であることが分かった.
実行中のプロセスを1つのテーブルで説明します.
スレッドA
スレッドB
a
読み出しa
読み出しa
1
a + 1
a+1、結果を書き戻す
2
結果を書き戻す
2
この現象が発生したのは,a=a+1が実は複数のステップで構成されているため,1つのスレッド操作の過程で他のスレッドも操作できるため,予想外のエラー結果が発生したためである.
したがって,1つのスレッドが操作共有変数を実行する際に,他のスレッドが操作できない,すなわち干渉できない場合に,プログラムが正常に動作することを保証できるのが原子性である.

可視性


≪可視性|Visibility|emdw≫:1つのスレッドがステータスを変更すると、他のスレッドに変更が表示されます.
コンピュータの構成原理を知ったことがある人は知っているはずで、CPUの高すぎる実行速度とメモリの低すぎる読取速度の矛盾を緩和するために、CPUはキャッシュ機能を内蔵して、最近アクセスしたデータを記憶することができて、もし再びこれらのデータを操作する必要があるならば、キャッシュから読み取るだけでいいので、メモリI/Oの時間を大幅に減らしました.
(ここにJVMのメモリ構造分析があり、追加する必要があります)
ただし、この場合、マルチプロセッサの場合、同じメモリ領域を操作すると、複数のプロセッサキャッシュにそのメモリ領域のコピーが存在するという問題が発生します.ただし、各プロセッサの結果に対する操作は、各プロセッサが独自のキャッシュ領域を読み出しているため、キャッシュが一致しない場合があります.
同じように小さな例で理解する
public class Main {
    private static Boolean ready = false;
    private static Integer number = 0;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (!ready) ;
            System.out.println(number);
        }).start();
        Thread.sleep(100);
        number = 42;
        ready = true;
        System.out.println("Main Thread Over !");
    }
}

ここでreadyはfalseに初期化され、trueの後にnumberの結果が印刷されるまでreadyの値を監視し続けるスレッドを作成します.プライマリ・スレッドは、スレッドの作成後にreadyとnumberに値を再割り当てします.
実行後、プログラムがMain Thread Over !を印刷したことは、メインスレッドが終了したことを意味し、readyとnumberはすでに付与されているはずだが、長い間待っていたが、numberの値が正常に印刷されていないことが分かった.
ここではメインスレッドでスレッドを一時停止させ、サブスレッドが先に実行されることを保証するため、サブスレッドが読み取ったメモリのreadyはfalseであり、自身のキャッシュにコピーされ、メインスレッドが実行されるとreadyの値が変更され、サブスレッドはこのイベントの発生を知らず、バッファ内の値が使用されている.これは,マルチスレッドでキャッシュが一致しないため,性の問題が見られる.
興味のある方はThread.sleep(100);このキャンセルは、結果を見て、原因を分析します.

秩序性


秩序性:プログラム実行の順序はコードの前後順に実行される.
これを見て理解できない学生もいるかもしれませんが、この関連例も与えにくいです.ランダム性が大きいからです.まず、なぜこれがあるのか理解してみましょう.プログラムの実行順序は私が書いたコードの順序ではありませんか.
実はそうとは限らない.
上述したように、各プロセッサにはキャッシュがあり、プログラムの実行中に、より多くの回数のヒットキャッシュは、より効率的な実行を意味することが多いが、キャッシュの空間は実際には小さく、新しい変数として使用する必要がある場合がある.これに対して、多くのコンパイラには最適化が内蔵されており、プログラムの実行結果に影響を与えないようにコードの一部の位置を調整することで、キャッシュの利用率を向上させることができます.
たとえば
Integer a,b;
a = a + 1; //(1)
b = b - 3; //(2)
a = a + 1; //(3)

プロセッサのキャッシュスペースが小さく、次の変数しか保存できない場合は、(3)文を(1)、(2)文の間に配置し、キャッシュを1回多く使用し、プログラムの実行結果を変更しません.これが再ソートの問題であり,もちろん再ソートが向上したのはキャッシュ利用率だけでなく,他にも多くの面がある.
ここまで来ると、プログラムの実行結果に影響を与えないことを保証して再ソートが発生するのではないかと疑問に思うかもしれませんが、なぜこの点を考慮しなければならないのでしょうか.
並べ替えはhappens-beforeの原則を遵守しているが、この原則は実際にはマルチスレッドの交互の状況を考慮していない.これは複雑すぎるため、マルチスレッドの交互性を考慮して実行結果に影響を与えないように並べ替えなければならない最善の方法は、並べ替えないことである:-)
happens-before原則
  • 同じスレッド内の各アクションは、その後のいずれかのアクションにhappens−beforeが現れる.
  • は、1つのモニタに対するhappens−beforeのロックを解除し、各後続の同じモニタに対するロックを追加する.
  • volatileフィールドへの書き込み操作happens-beforeの後続の同じフィールド毎の読み取り操作.
  • Thread.start()の呼び出しは、起動スレッド内のhappens-beforeの動作を開始します.
  • Threadのすべての動作はhappens-beforeが他のスレッドでこのスレッドの終了またはThreadをチェックする.join()で戻るかThread.isAlive()==false.
  • あるスレッドAが別のスレッドBを呼び出すinterrupt()は、いずれもhappens-beforeがスレッドAにBがAによって中断されていることを発見した(Bが異常を放出したか、AがBを検出したisInterrupted()またはinterrupted()である.
  • オブジェクト構築関数の終了happens-beforeとそのオブジェクトのfinalizerの開始
  • A動作happens-beforeがBで動作し、B動作happens-beforeがCで動作する場合、A動作happens-beforeはCで動作する.

  • では、マルチスレッドでの再ソートは、プログラムの結果にどのように影響しますか?例を挙げてみましょう
    public class Main {
        private static volatile Boolean ready = false;
        private static volatile Integer number = 0;
    
        public static void main(String[] args) throws InterruptedException {
            new Thread(() -> {
                while (!ready) ;
                System.out.println(number);
            }).start();
            number = 42; //(1)
            ready = true; //(2)
            System.out.println("Main Thread Over !");
        }
    }
    

    注意ここでは、スレッドがスリープしているコードが削除されています.
    ここでは理想的な状況を仮定し,プログラム全体が可視性を満たしている(ここではvolatileを用い,具体的な原理は続文を可視化する)と仮定し,このとき再ソートが発生し,(1)(2)2行の内容を交換し,サブスレッドが実行を開始し,readyの検出を継続した.メインスレッド実行は、並べ替えが発生したため(2)が先に実行され、サブスレッドはreadyがtrueになるのを見てnumberの値を印刷し、numberの値は0であり、予想される結果は42である.
    これがマルチスレッドの場合にプログラム実行を要求する順序がコードの前後順に実行される理由の一つである.