Java マルチスレッドのロック メカニズムの復号化: CAS と Synchronized の動作原理と最適化戦略

CAS

CASとは

CAS: 正式名は Compare and swap で、文字通りの意味は「比較と交換」です。CAS には次の操作が含まれます:
メモリ内の元のデータが A、古い期待値が B、および変更する必要がある値であると仮定します。修正はCです。

  1. まず、A と B を比較して、A と B が同じかどうかを確認します。
  2. A と B が同じ場合、データ C の値を A に代入します。
  3. 返却操作が成功しました。

CAS をより深く理解するために、CAS 疑似コードを書いてみましょう。

 boolean Cas(int a,int b,int c){
    
    
        //进行比较看a是否发生变化
        if(a==b){
    
    
            a=c;
            return true;
        }
       return false;
    }

CAS は楽観的ロックの実装方法であり、複数のスレッドがデータを操作する場合、1 つのスレッドだけが正常に動作し、他のスレッドはブロックされず、操作が失敗したことを示すシグナルを返します。
実際の CAS はアトミックなハードウェア命令によって完成され、ハードウェアがサポートしている場合にのみソフトウェアを実装できます。

CASの適用

標準ライブラリには java.util.concurrent.atomic パッケージが用意されており、その中のクラスはすべてこのメソッドに基づいて実装されています。
典型的な例は AtomicInteger クラスで、このクラスでは getAndIncrement が i++ 操作に相当します。

public static void main(String[] args) {
    
    
        ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
        AtomicInteger  seq = new AtomicInteger(0);
        //进行++操作
        seq.getAndIncrement();
        seq.getAndIncrement();
        seq.getAndIncrement();
        System.out.println(seq);
    }

ここに画像の説明を挿入します
auto-increment メソッドをクリックすると、その操作も上記の疑似コードによって実装されていることがわかります。
ここに画像の説明を挿入します
CAS を使用してスピン ロックを実装することもできます

ABA問題

2 つのスレッド t1 と t2 があるとします。初期値が A の共有変数 num があります。
次に、スレッド t1 は CAS を使用して num 値を Z に変更したいと考えています。

  • まず num の値を読み取り、oldNum 変数に記録します。
  • CAS を使用して、num の現在の値が A であるかどうかを確認します。A の場合は、Z に変更します。
    ただし、t1 によるこれら 2 つの操作の実行の間に、t2 スレッドは num の値を A から B に変更し、再び B から A に変更する可能性があります。

例外の例

銀行からお金を引き出す場合を例に挙げます。

  1. デポジット 100、スレッド 1 は現在のデポジット値が 100 であることを取得し、それが 50 に更新されることを期待し、スレッド 2 は現在のデポジット値が 100 であることを取得し、それが 50 に更新されることを期待します。
  2. スレッド 1 は控除を正常に実行し、デポジットは 50 に変更されます。スレッド 2 はブロックされて待機しています。
  3. スレッド 2 が実行される前に、友人はちょうど 50 をあなたに送金し、アカウント残高は 100 になります。
  4. スレッド 2 が実行する番になり、現在のデポジットが 100 であることがわかり、これは以前に読み取られた 100 と同じであり、減算操作が再度実行されます。

このままでは私たちのお金がなくなってしまうのですから、この状況は絶対に許せません。

そこで、この問題を解決するためにバージョン番号を導入しました。CAS は古い値を読み取るときにバージョン番号も読み取ります。変更する場合、読み取ったバージョン番号が現在のバージョン番号と同じである場合、変更が行われます。現在のバージョン番号が読み取ったバージョン番号より大きい場合、変更は行われます。失敗します。

同期原理

基本的な機能

  1. 楽観的ロックから開始され、ロックの競合が深刻な場合は悲観的ロックにアップグレードされます。
  2. 同期はリエントラントロックです。
  3. 不当なロックです。
  4. 読み取りと書き込みが不可能なロックです
  5. 軽量ロックの実装から開始され、ロックが長時間保持されると、重量ロックに変換されます。

ロック処理

ロックのフローチャート:
ここに画像の説明を挿入します

バイアスロック

バイアスされたロックは、現在のロック オブジェクト内でロックがどのスレッドに属しているかをマークすることです。実際のロックは実行されません。ロックなしで実行できる場合はロックされず、不要なオーバーヘッドが削減されます。他のスレッドがロックを競合する場合にのみ、ロックはバイアスロックから軽量ロックにアップグレードされます。

軽量ロック

ロックが軽量ロックにアップグレードされた後、CAS を通じて実装されます。

  • CAS 経由でメモリのブロックを確認し、更新します。
  • 更新が成功すると、ロックは成功したとみなされます。
  • 更新に失敗した場合は、ロックが失敗し、ロックが占有されているとみなされます。

重量級ロック

競合が激化し、スピンがすぐにロック状態を取得できなくなると、
カーネルが提供するミューテックスを使用する重量ロックに発展します。

  • ロック操作を実行するには、まずカーネル状態に入ります。
  • 現在のロックがカーネル モードで占有されているかどうかを確認します。
  • ロックが占有されていない場合、ロックは成功し、システムはユーザー モードに戻ります。
  • ロックが占有されている場合、ロックは失敗します。この時点で、スレッドはロックの待機キューに入り、ハングします。オペレーティング システムによってウェイクアップされるのを待っています。
  • 一連の浮き沈みを経た後、他のスレッドによってロックが解放されましたが、オペレーティング システムも中断されたスレッドを記憶していたので、スレッドを起動してロックを再取得しようとしました。

複数のスレッドが同じロックをめぐって競合し、スピン待機時間が長すぎてロックを取得できない場合、JVM はロックを重量ロックにアップグレードします。この時点で、スレッドはスピンして待機することはなくなり、カーネル状態に入り、オペレーティング システムが提供するミューテックス実装を通じてロック ステータスと待機キューを管理します。
カーネル モードでは、オペレーティング システムは現在のロックがすでに占有されているかどうかを判断します。ロックが占有されていない場合、スレッドはロックを正常に取得し、ユーザー モードに戻って実行を継続します。ロックがすでに占有されている場合、スレッドはロックに失敗します。この時点で、スレッドはロック待機キューに入り、オペレーティング システムによって中断され、ウェイクアップされるのを待ちます。
時間が経ち、スレッドが競合し、他のスレッドがロックを解放し、スレッドがロックを待機していることをオペレーティング システムが認識すると、オペレーティング システムは待機中のスレッドを起動し、再起動してロックの再取得を試みます。このプロセスには時間がかかる場合があり、その後、スレッドは実行を続行するために再度ロックを取得しようとします。

その他の最適化操作

ロックの解除

コンパイラ + JVM はロックを削除できるかどうかを判断し、削除できる場合は直接削除します。
言い換えれば、ロック操作の多くが単一スレッドで実行される場合、それらのロック操作のためのロックは必要ありません。

 @Override
    public synchronized StringBuffer append(String str) {
    
    
        toStringCache = null;
        super.append(str);
        return this;
    }

たとえば、StringBuffe の追加操作にはロック操作が含まれますが、シングルスレッド操作ではロックを削除できます。

ロックの粗面化

ロジックの一部で複数のロックとロック解除が発生した場合、コンパイラーと JVM は自動的にロックを粗くします。

授業で使用した例は次のとおりです。

リーダーは以下の人々にタスクを割り当てます。タスクは合計 3 つあります。現在、2 つの方法があります。

  1. 従業員に電話をかけ、一度に 3 つのタスクを実行します。
  2. 一度に 1 つのタスクで、従業員に 3 回電話をかけます。

もちろん、他の JVM もこのようなロック粗密化を実行するでしょう。

コードで理解できます。

        //频繁加锁
        for (int i = 0; i < 100; i++) {
    
    
            synchronized (o1){
    
    
            }
        }
        //粗化
        synchronized (o1){
    
    
            for (int i = 0; i < 100; i++) {
    
    
            }
        }

頻繁にロックを適用したり解放したりすることを避けるために、ロックを粗くします。

おすすめ

転載: blog.csdn.net/st200112266/article/details/133085307