Java マルチスレッド シリーズ V (共通ロック戦略 + CAS + 同期原則)


1.楽観的ロックと悲観的ロック

ロックの実装者は、次のロック競合の確率を予測して、次に何をするかを決定します。したがって、それは2つの主要な「学校」に分かれています。

オプティミスティック ロック: オプティミスティック ロックは楽観的なアイデアです。その後競合が発生する可能性が低い、または複数のスレッド間で競合が発生しないと予測します。そのため、データにアクセスするときはロックしませんが、データを読み取るときに記録します。バージョン番号。データの更新時にバージョン番号が一致しない場合は、データが他のスレッドによって変更されたと見なされ、更新を再試行する必要があります (現在のデータ アクセスが競合するかどうかを識別するには、バージョン番号またはタイムスタンプを使用します)。たとえば、Java の AtomicInteger クラスは、内部実装でオプティミスティック ロック メカニズムを使用します。

悲観的ロック: 悲観的ロックは悲観的な考え方です。競合の可能性が比較的高いか、複数のスレッド間で競合が発生することを予測します。そのため、データにアクセスするときにデータをロックして、他のスレッドが同時にアクセスできないようにします。時間。

Synchronized は最初は楽観的なロック戦略を使用しますが、ロックの競合が頻繁に発生することが判明すると、自動的に悲観的なロック戦略に切り替わります。

一般に、悲観的なロックはより多くの作業を実行する必要があり、効率が低くなります。楽観的ロックは作業量を減らし、効率を高めます。

2. 重量ロックと軽量ロック

知識補足:ロックの核となる機能は「原子性」であり、そのメカニズムはCPUなどのハードウェアデバイスによって提供されることに遡ります。CPU は「アトミック操作命令」を提供し、オペレーティング システムはmutexCPU のアトミック命令に基づいてミューテックス ロックを実装します。JVM は、オペレーティング システムによって提供されるミューテックス ロックに基づいて、synchronized や ReentrantLock などのキーワードとクラスを実装します。

提供原子操作指令
提供mutex互斥锁
提供synchronized等关键字
CPU
操作系统
JVM
Java代码

ヘビーウェイト ロック: ロックとロック解除のプロセスがより効率的になります。ロック メカニズムは、OS によって提供されるミューテックスに大きく依存します。これには多数のカーネル モードとユーザー モードの切り替えが含まれるため、スレッド スケジューリングが容易に発生する可能性があります。この操作は比較的高価です。

軽量ロック: ロックとロック解除のプロセスがより効率的になります。ロック機構は可能な限りミューテックスを使用せず、ユーザーモードコードを使用して完了するようにしてください。本当に理解できない場合は、もう一度 mutex を使用してください。これには少量のカーネル モード ユーザー モードの切り替えが含まれますが、これによりスレッド スケジューリングが発生するのは容易ではありません。

一般に、楽観的なロックは軽量ロック (絶対的ではない) である可能性が高く、悲観的ロックは重量級ロック (絶対的ではない) である可能性が高いです。

用户态の時間コストは比較的制御可能ですが、内核态の時間コストは制御可能ではないことに注意してください。

ユーザー モードのプログラムは、ユーザー空間のデータとコードにのみアクセスでき、カーネル空間のデータとリソースに直接アクセスできません。一方、カーネル モードのプログラムは、すべてのシステム リソースにアクセスして操作できます。

ユーザー モードとカーネル モード間の切り替えは、システム コールを通じて実装する必要があります。つまり、ユーザー モードからカーネル モードに移行し、カーネル コードを実行して一部の特権操作を完了し、結果をユーザー モードに返します。この切り替えプロセスには多くの時間とリソースが必要となるため、ユーザー モードとカーネル モード間の切り替え回数を減らすことは、システム パフォーマンスを最適化する重要な方法です。

3. スピンロック&サスペンドウェイトロック

スピンとブロック:
スピンは、待機中のため、できるだけ早くロックを取得するために実装されています。ブロックして待機するということは、現在の CPU を使用する権利を放棄することを意味し、後でスレッドが目覚めたとしても、スレッドができるだけ早く CPU を再び取得できるという保証はありません。

スピン ロック: これは、軽量ロックの典型的な実装です (通常はユーザー モードで保存され、カーネル モードを経由する必要はありません)。
スピン ロックとは、現在のスレッドがロック フラグを繰り返しチェックすることを意味します。フラグが別のスレッドによって設定されていることが判明した場合、スレッドはロックを取得するまでループでチェックを続けます。スピン ロックは、共有データ領域へのアクセスが短く、競争の激しさが高くない状況に適しています。スピン待機では、他のスレッドが使用できるように実際に CPU を解放するわけではありませんが、ループ チェックのために常に CPU を占有するためです。スピンの待機時間が長すぎると、CPU リソースが無駄になり、システムのパフォーマンスに影響します。

一時停止待機ロック (ブロッキング ロックとも呼ばれます) : これは、重量ロックの典型的な実装です。(一時停止待機は通常、カーネル機構を通じて実装されます)
一時停止待機ロックとは、スレッドがロックを要求したときに、そのロックがすでに別のスレッドによって保持されていることが判明した場合、そのスレッドは一時停止され、ロックが解放されるまで待機することを意味します。 。サスペンドしてロックを待機するプロセス中に、スレッドはスリープ状態に入り、他のスレッドが使用できるように CPU リソースを解放します。ロックを保持しているスレッドがロックを解放すると、待機していたスレッドが起動され、再度ロックを要求します (ロックはすぐに取得できない場合があり、ロック競合を再実行する必要があります)。

一般に、スピン ロックは、スレッド コンテキストの切り替えによって引き起こされるオーバーヘッドを回避するために、共有データ領域への短期アクセスと競合強度の低い場合に適していますが、ハングウェイト ロックは共有データ領域への長期アクセスと競争強度の高い場合に適しています。この場合、CPU リソースを有効に活用でき、CPU のアイドル時間を削減できます。

4. ミューテックスロックと読み書きロック

Mutex : 複数のスレッドが同時に共有リソースにアクセスすることを防ぐために、マルチスレッド プログラミングで使用されるメカニズムです。複数のスレッドがアクセスしたいものをロックで囲むことにより代码区域、1 つのスレッドのみがロックを保持でき、他のスレッドは実行を続行する前にそのスレッドがロックを解放するまで待機する必要があります。通常、ミューテックス ロックは、共有リソースへのシングル スレッド アクセスを保護し、データの正確性を確保しながらプログラムの効率を確保するために使用されます。

ReadWrite Lock : これは、複数のスレッドが共有リソースを同時に読み取ることを可能にする、より高度な同期メカニズムですが、共有リソースに書き込むときにミューテックス ロックの保護が必要です。

読み取り/書き込みロックの実装では、共有リソースを読み取る場合、複数のスレッドが同時に読み取りロックを占有することができます。共有リソースに書き込む場合、1 つのスレッドのみが書き込みロックを占有することができ、他のスレッドはそれを待つ必要があります。書き込みロックを解除します。ロックを取得します。読み取り/書き込みロックの使用シナリオは一般的に次のとおりです。读操作频繁,但写操作比较少的场景,如数据库、文件系统等。

  1. 読み取りロックと読み取りロックの間には相互排他はありません。
  2. 書き込みロックと書き込みロックは相互に排他的です。
  3. 読み取りロックと書き込みロックの間には相互排他があります。

Java 標準ライブラリは、読み取り/書き込みロックを実装する ReentrantReadWriteLock クラスを提供します。

  • ReentrantReadWriteLock.ReadLock クラスは読み取りロックを表します。このオブジェクトは、ロックおよびロック解除のための lock/unlock メソッドを提供します。
  • ReentrantReadWriteLock.WriteLock クラスは書き込みロックを表します。このオブジェクトは、ロックおよびロック解除のための lock/unlock メソッドも提供します。

5. リエントラントロックと非リエントラントロック

リエントラント ロック: リエントラント ロックは文字通り「再入できるロック」を意味し、同じスレッドがブロックされることなく同じロックを複数回取得できるようにします。この種のロックにより、同じスレッドが同じロックを取得した場合のデッドロック状態の発生を回避できると同時に、コードの効率と正確性が保証されます。

たとえば、再帰関数にロック操作がある場合、再帰プロセス中にロック自体がブロックされますか? そうでない場合、ロックはリエントラント ロックです

ReentrantLock は、再入可能ロックの一般的な実装です。カウンタを使用して、ロックが保持されている回数を追跡します。スレッドがロックを取得するたびに、カウンタは 1 ずつ増分され、ロックが解放されると、カウンタは 1 ずつ減分されます。 1.

非リエントラント ロック: スレッドがロックを取得した後、ロックが解放されるまで、再度ロックを取得する要求がブロックされることを意味します。この種のロックは通常、デッドロック状況を引き起こします。これは、スレッドがロックを取得し、引き続きロックを取得すると予想される場合、スレッドは自身のロックが解放されるまで待機するためです。

: 同期キーワード ロックを含む、JDK によって提供されるすべての既製のロック実装クラスは再入可能です。

6. 公平なロックと不公平なロック

合意: 先着順でロックが守られれば公平ロック、守られなければ不公平ロックとなります。

: システムのスレッド スケジューリングはランダムであり、同期ロックは不公平です。

7. CAS

1. CASの特徴

CAS: 正式名は Compare and swap で、文字通りには「比較と交換」を意味します。
CAS(V,A,B); CAS 操作には、メモリ位置 V、期待値 A、新しい値 B の 3 つのパラメータが含まれます。その実行プロセスは次のとおりです。

  1. まず、メモリ位置 V の値を比較して、それが期待値 A と等しいかどうかを確認します。
  2. それらが等しい場合、メモリ位置 V の値は新しい値 B に置き換えられ、操作は成功します。それ以外の場合、操作は失敗します。
  3. 操作が成功したかどうかに関係なく、メモリ位置 V の現在値が返されます。

注意してください:

  1. CAS は原子的ハードウェア命令によって完了します。CAS のメモリ読み取り、比較、および書き込みメモリ操作はハードウェア命令であり、アトミックです。
  2. CAS はレジスタを操作するのではなく、メモリを直接読み書きします。
  3. 複数のスレッドがリソースに対して CAS 操作を同時に実行する場合、正常に操作できるのは 1 つのスレッドだけですが、他のスレッドはブロックされず、他のスレッドは操作が失敗したというシグナルのみを受信します。CASは、楽観的ロックの一種と考えることもできるし、楽観的ロックの実装方法であると理解することもできる。

2. CASの適用

アトミック クラスの実装:
標準ライブラリ java.util.concurrent.atomic のクラスは、CAS (Compare-And-Swap) テクノロジを使用して実装されます。

たとえば、AtomicInteger クラスでは、これらのクラス自体がアトミックであるため、関連する操作はマルチスレッド下でも安全です。

  1. num.getAndIncrement(); // この操作は num++ と同等です

  2. num.incrementAndGet(); // この操作は ++num と同等です

  3. num.getAndDecrement(); // この操作は num- と同等です

  4. num.decrementAndGet(); // この操作は –num と同等です

テストのアトミック クラス:

public class CAS {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        // 创建原子类,初始化值为0
        AtomicInteger num = new AtomicInteger(0);

        Thread t1 = new Thread(()->{
    
    
            for (int i = 0; i < 50000; i++) {
    
    
                // num++ 操作
                num.getAndIncrement();
            }
        });

        Thread t2 = new Thread(()->{
    
    
            for (int i = 0; i < 50000; i++) {
    
    
                num.getAndIncrement();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(num);

    }
}

num.getAndIncrement(); 操作の疑似コード:

class AtomicInteger {
    
    
    private int value;
    public int getAndIncrement() {
    
    
        int oldValue = value;
        while ( CAS(value, oldValue, oldValue+1) != true) {
    
    
            oldValue = value;
       }
        return oldValue;
   }
}

:ここでのCAS演算におけるパラメータは、oldValueワーキングメモリ(レジスタ)上の値とvalueメインメモリ上の値とみなすことができます。value と oldValue の値が同じである場合、つまり、この更新中に value の値が変更されていない場合、自動インクリメントを実現するために、oldValue+1 の値が value に割り当てられます。比較中に値が oldValue と等しくない場合は、この更新操作中に値が変更されたことを意味するため、この更新は失敗し、次の更新操作のために oldValue 値がリフレッシュされます。

3. CAS はスピンロックを実装します

CAS を使用してスピン ロック擬似コードを実装します。

public class SpinLock {
    
    
    private Thread owner = null;
    public void lock(){
    
    
        // 通过 CAS 看当前锁是否被某个线程持有. 
        // 如果这个锁已经被别的线程持有, 那么就自旋等待. 
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. 
        while(!CAS(this.owner, null, Thread.currentThread())){
    
    
       }
   }
    public void unlock (){
    
    
        this.owner = null;
   }
}

原理説明:上記のCAS擬似コードは、現在のロック保持者が空であれば比較的成功し、現在のスレッドにロック取得権を与えることができ、ロック完了サイクルが終了することを示しています。空でない場合はowner、現在のロックが他のスレッドによって保持されていることを意味します。この時点で、CAS 操作は失敗し、アイドリング ループに入り、現在のロック ホルダーが空かどうかを継続的に尋ねます。このとき、他のスレッドが解放されると、ロックを取得すると、現在のスレッドはすぐにロックを取得できます。

4. CASのABA問題

CASは値が同じかどうかを比較することしかできず、途中で値が変わったかどうかを判断することはできません。これにより、スレッドが値を操作するときに誤った判断や誤った結果が発生する可能性があります。

たとえば、スレッド T1 は値 A を持つメモリ位置 V を読み取り、いくつかの操作を実行し、最後に値を B に更新します。この期間中、スレッド T2 は V の値を A から C に変更し、A に戻します。このとき、T1 が再度 CAS 操作を実行すると、V の値が A のままであることがわかり、他のスレッドによって変更されていないと考え、V の値を B に更新します。しかし実際には、V の値はこのプロセス中に他のスレッドによって変更されており、ABA の問題が発生します。

たとえば、出金操作を実行する場合: 現在 100 元の預金があり、50 元を引き出す必要があるとします。このとき、出金操作を実行するために 2 つのスレッドが確立されます。通常の状況では、スレッド 1 とスレッド 2 が現在のデポジット 100 元を読み取り、スレッド 1 がそれ​​を 50 元に変更し、引き落としが成功します。スレッド 2 はブロックされて終了します。現在のデポジット 50 元と 100 元の差を比較すると、スレッド 2 は失敗します。しかし、スレッド 1 で控除が成功した後、誰かが突然さらに 50 元を私に送金し、デポジットが再び 100 元になった場合、スレッド 2 で控除操作が開始されると、デポジットと読み取り値が判明します。は同じです、そしてそれは再び起こります 減点。これはABAの問題です。

解決策は、
给要修改的数据引入版本号。 CAS がデータの現在の値と古い値を比較する一方で、バージョン番号が期待を満たしているかどうかも比較する必要があるということです。現在のバージョン番号が以前に読み取られたバージョン番号と一致していることが判明した場合、変更は行われます。実際に操作が実行され、バージョン番号がインクリメントされます。現在のバージョン番号が以前に読み取られたバージョン番号より大きいことが判明した場合、操作は失敗したとみなされます。

8. 同期原理

1. シンクロナイズドの基本特性

上記のロック戦略を組み合わせると、Synchronized には次の特性があると結論付けることができます。

  1. 楽観的ロックで開始され、ロックの競合が頻繁に発生する場合は悲観的ロックに変換されます。
  2. 軽量ロックの実装から開始され、ロックが長時間保持されると、重量ロックに変換されます。
  3. 軽量ロックはスピン ロックに基づいて部分的に実装され、重量ロックは保留待機ロックに基づいて部分的に実装されます。
  4. リエントラントロックです。
  5. 読み取り/書き込みロックではありません。
  6. 正しいロックと間違ったロック。

2. 同期ロックのアップグレード戦略

前述の同期ロックの戦略から、実際のシナリオに従って同期ロックをアップグレードできることがわかります。JVM では、主に次の同期ロックのロック アップグレード戦略があります。

遇到锁竞争
锁竞争更激烈
无锁
偏向锁
轻量级锁
重量级锁

バイアスされたロックの概念は、上記のロック戦略に組み込まれています。

バイアスロック:必要な場合以外はロックしないでください。バイアス ロックは本当の「ロック」ではなく、ロックがどのスレッドに属しているかを記録するためにオブジェクト ヘッダーに「バイアス ロック マーク」を置くだけです。

  1. コード実行プロセス全体で、現在マークされているオブジェクトのロックを競合するスレッドが他にない場合、現時点では実際にロックする必要はありません。これにより、ロックとロック解除によって生じるオーバーヘッドが節約されます。
  2. 如果后续有其他线程来竞争该锁,由于已经在该锁对象中记录了当前锁属于哪个线程了,因此很容易识别当前申请锁的线程是不是之前记录的线程,如果不是,那就取消原来的偏向锁状态,进入一般的轻量级锁状态。

简单来说,偏向锁就相当于是“搞暧昧”,一旦发现潜在危险,就立即官宣!

总之synchronized的锁升级策略主要指:当一个线程访问共享资源时,秉承非必要不加锁, 优先进入偏向锁状态。随着其他线程进入竞争,偏向锁状态被消除,进入轻量级锁状态。如果竞争进一步激烈,自旋不能快速获取到锁状态,就会膨胀为重量级锁。

3、synchronized 锁优化操作

锁消除
在程序中,可能存在有些程序的代码,用到了 synchronized,但其实没有在多线程环境下。此时这些加锁操作是非常没有必要的,而且会白白浪费加锁和解锁的资源开销。(如单线程下使用StringBuffer)这时我们的编译器+JVM 就会判断锁是否可消除,如果可以,就直接消除。

锁粗化
在一代码段逻辑中,如果出现多次加锁解锁,编译器 + JVM 会自动进行锁的粗化。

这里的锁粗化(细化)是相对于锁的粒度的,锁粒度即synchronized代码块包含代码的多少(代码越多,粒度越粗。越少粒度越细)。一般写代码的时候,多数情况下,希望锁的粒度更小一点(串行执行的代码少一些,并发执行的代码多一些,充分利用CPU内核资源)。但是实际上可能并没有其他线程来抢占这个锁进行并发,这种情况 JVM 就会自动把锁粗化,避免频繁申请释放锁带来额外开销。

おすすめ

転載: blog.csdn.net/LEE180501/article/details/130546165