Java Face Classic 02 - 同時実行記事 - スレッド 6 状態、スレッド プール、待機とスリープ、ロックと同期、揮発性、悲観的ロックと楽観的ロック、ハッシュテーブル、ThreadLocal

同時記事

1. スレッドの状態

必要とする

  • Java スレッドの 6 つの状態をマスターする
  • Java スレッドの状態遷移をマスターする
  • 5つの州と6つの州の違いを理解できる

6 つの状態と遷移

ここに画像の説明を挿入

それぞれ

  • 新しいビルド
    • スレッド オブジェクトが作成されたが、start メソッドが呼び出されなかった場合、スレッド オブジェクトは新しい状態になります。
    • 現時点ではオペレーティング システムの基礎となるスレッドに関連付けられていません
  • 実行可能
    • start メソッドを呼び出した後、新しく作成されたメソッドから実行可能ファイルに入ります。
    • 現時点では、基礎となるスレッドに関連付けられており、オペレーティング システムによる実行がスケジュールされています。
  • 終わり
    • スレッド内のコードが実行され、実行可能からファイナライズに入ります。
    • このとき、基となるスレッドとの関連付けは解除されます。
  • ブロック
    • ロックの取得が失敗すると、モニターに実行できるブロッキング キューがブロックされ、現時点では CPU 時間は占有されません。
    • ロック保持スレッドがロックを解放すると、特定のルールに従ってブロッキング キュー内のブロックされたスレッドがウェイクアップされ、ウェイクアップされたスレッドは実行可能状態になります。
  • 待って
    • ロックの取得に成功したものの、条件が満たされていない場合は、 wait() メソッドが呼び出されます。このとき、ロックは実行可能状態から解放され、 wait に設定されたモニター待機入ります。 CPU時間。
    • 他のロック保持スレッドがnotify()またはnotifyAll()メソッドを呼び出すと、待機セット内の待機スレッドが特定のルールに従ってウェイクアップされ、実行可能な状態に復元されます。
  • 制限時間待ち
    • ロックの取得に成功したものの、条件を満たしていない場合は wait(long) メソッドが呼び出され、このときロックは実行可能状態から解放され、時限待ちの監視待ち状態に移行ます。 CPU時間を占有しません。
    • 他のロック保持スレッドがnotify()またはnotifyAll()メソッドを呼び出すと、待機セット内の時間制限付き待機スレッドが特定のルールに従ってウェイクアップされ、実行可能な状態に復元され、ロックをめぐって再競合されます。
    • 待機がタイムアウトになった場合も、時間制限付き待機状態から実行可能な状態に回復し、ロックを再競合します。
    • もう 1 つの状況は、sleep(long) メソッドを呼び出すと、実行可能状態から時間制限付きの待機状態に入りますが、これはモニターとは何の関係もなく、積極的にウェイクアップする必要はありません。自然に実行可能な状態に戻ります

その他の状況 (知っておく必要があるだけ)

  • Interrupt() メソッドを使用すると時間制限のある待機中のスレッドを中断し、実行可能な状態に戻すことができます。
  • パーク、パーク解除、その他のメソッドでもスレッドを待機させたり、ウェイクアップさせたりすることができます
  • コードデモ:
public class TestThreadState {
    
    
    // 唯一锁对象
    static final Object LOCK = new Object();

    public static void main(String[] args) {
    
    
        testNewRunnableTerminated();
    }

    private static void testNewRunnableTerminated(){
    
    

        // Runnable方式+lambda表达式创建多线程
        Thread t1 = new Thread(()->{
    
    
            System.out.println("running...");//3
        },"t1");

        System.out.println("state: "+t1.getState());//1
        t1.start();
        System.out.println("state: "+t1.getState());//2

        System.out.println("state: "+t1.getState());//4
    }
}

コードのデモ: デバッグ モードのみが効果をよりよく確認できます。デバッグはマルチスレッドの実行シーケンスを制御できるため、マルチスレッドはスレッド デバッグ モードを選択できることに注意してください。 マルチスレッド関連コード デバッグ
モード
ここに画像の説明を挿入

クリックしてスレッドを切り替えると、特定の実行場所が表示されます。
ここに画像の説明を挿入

t1 に切り替えて直接解放し、メイン スレッドの前に t1 の実行を終了させます。そうするとメイン スレッドは最終的な最終状態を出力します。
ここに画像の説明を挿入

ここに画像の説明を挿入

実際、これを次のメソッドに変更すると、t1 スレッドがメイン スレッドよりも先に完了することが保証されます (最初にメイン スレッドを 1 秒間スリープさせておくと、それほど面倒なデバッグは必要ありません)。

public class TestThreadState {
    
    
    // 唯一锁对象
    static final Object LOCK = new Object();

    public static void main(String[] args) {
    
    
        testNewRunnableTerminated();
    }

    private static void testNewRunnableTerminated() {
    
    

        // Runnable方式+lambda表达式创建多线程
        Thread t1 = new Thread(()->{
    
    
            System.out.println("running...");//3
        },"t1");

        System.out.println("state: "+t1.getState());//1
        t1.start();
        System.out.println("state: "+t1.getState());//2

        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            throw new RuntimeException(e);
        }
        System.out.println("state: "+t1.getState());//4
    }
}

ここに画像の説明を挿入

  • コードデモ 2: ブロックされました
private static void testBlocked() throws InterruptedException {
    
    
    Thread t2 = new Thread(() -> {
    
    

        System.out.println("before sync");
        synchronized (LOCK){
    
    //3 被锁住
            System.out.println("in sync");//6 
        }
    },"t2");
    t2.start();
    System.out.println("state: "+t2.getState());//1   RUNNABLE
    synchronized (LOCK){
    
    //2 先进来
        System.out.println("state: "+t2.getState());//4  BLOCKED
    }//5 出来释放锁
    System.out.println("state: "+t2.getState());//7 RUNNABLE
}

ここに画像の説明を挿入
デバッグ モードは、コード行の後にマークされているように実行シーケンスを制御します。t2 が最初であることがわかりますRUNNABLE->BLOCKED。次に、BLOCKED->RUNNABLE

  • コードデモ 3: 待機中
private static void testWaiting() {
    
    
   Thread t2 = new Thread(() -> {
    
    
       synchronized (LOCK) {
    
    //注意也要加锁
           System.out.println("before waiting");//2
           try {
    
    
               LOCK.wait();//3  这里一旦wait,就立刻释放了锁 下面主线程就可以进入 锁代码块了
           } catch (InterruptedException e) {
    
    
               e.printStackTrace();
           }
       }
   }, "t2");


   t2.start();
   System.out.println("state: " + t2.getState());//1  RUNNABLE
   synchronized (LOCK) {
    
    
       System.out.println("state: " + t2.getState());//4 WAITING
       LOCK.notify();//5
       System.out.println("state: " + t2.getState());//6 BLOCKED(锁被主线程占了)
   }//这里一结束并释放锁,上面t2线程立刻被自动解锁 BLOCKED->RUNNABLE
   System.out.println("state: " + t2.getState());//7 RUNNABLE
}

ここに画像の説明を挿入

5つの州

5 つの州の声明は、オペレーティング システム レベルの分割に由来しています。
ここに画像の説明を挿入

  • 実行状態: CPU 時間に割り当てられ、実際にスレッドでコードを実行できます。
  • 準備完了状態: CPU 時間の対象ですが、まだ順番が来ていません
  • ブロック状態: CPU 時間の対象外
    • Java 状態で言及されているブロッキング待機時間指定待機をカバーします。
    • より多くのブロッキング I/O が存在します。つまり、スレッドがブロッキング I/O を呼び出すと、実際の作業は I/O デバイスによって完了します。この時点では、スレッドは何もすることがなく、待機することしかできません。
  • 新しい最終状態: Java の同じ名前の状態に似ていますが、冗長ではありません。

Java の実行可能状態 Runnable には、OS の準備完了、実行中、およびブロック I/O が含まれます (Java コードはブロック I/O と入力待機を区別できないため、実行中であると見なされます)。
ここに画像の説明を挿入

2. スレッドプール

必要とする

  • スレッド プールの 7 つのコア パラメータをマスターする

7 つのパラメータ

  1. corePoolSize コア スレッドの数 - プール内に保持されるスレッドの最大数 (0 にすることもできます。つまり、実行後にすべてのスレッドが保持されるわけではありません)
  2. MaximumPoolSize スレッドの最大数 = コア スレッド + 緊急スレッドの最大数
  3. keepAliveTime 生存時間 - レスキュー スレッドの生存時間。生存時間内に新しいタスクがない場合、このスレッド リソースは解放されます。
  4. 単位時間単位 - 緊急スレッドの生存時間単位 (秒、ミリ秒など)。
  5. workQueue - アイドル状態のコア スレッドがない場合、新しいタスクはこのキューに入れられ、キューがいっぱいになると、タスクを実行するための緊急スレッドが作成されます。
  6. threadFactory スレッド ファクトリ - スレッド名の設定、デーモン スレッドかどうかなど、スレッド オブジェクトの作成をカスタマイズできます。
  7. ハンドラー拒否戦略 - すべてのスレッド (緊急スレッドを含む) がビジーで、workQueue がいっぱいの場合、拒否戦略がトリガーされます。
    1. 例外 java.util.concurrent.ThreadPoolExecutor.AbortPolicy をスローする
    2. タスクは呼び出し側スレッド java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy によって実行されます(このスレッドのタスク コードは、t.start() を呼び出すスレッドによって実行されます。これは t.run() と同等です。スレッド プールがいっぱいの場合、複数のスレッドを作成する必要はなく、独自のスレッドで実行するだけです (例: main で呼び出されたものはメインスレッドで直接実行されます)
    3. タスク java.util.concurrent.ThreadPoolExecutor.DiscardPolicy を破棄します (例外を報告せず、実行もせず、サイレントに破棄します)
    4. キューに入れられた最も古いタスクを破棄します java.util.concurrent.ThreadPoolExecutor.DiscardOldestPolicy (タスク キューの先頭にあるタスクを破棄し、キューの最後に me を挿入します)

コアスレッド:タスク実行後、スレッドプールに保持する必要があるスレッド
緊急スレッド:タスク実行後、スレッドプールに保持する必要はない
緊急スレッド:スレッドプールに上限ありコアスレッド、タスクキューにも上限があり、ビジー時はブロッキングキュー(タスクキューworkQueueとも呼ばれます)も満杯となり、この時点で緊急スレッドが作成されます。緊急スレッドは、割り当てられたタスクを実行した後、キュータスクのタスクが実行されるまで、タスクキュー内のスレッドを見つけて実行し続けます(もちろん、緊急スレッドによって割り当てられたタスクはそう簡単に実行されるわけではありませんが、緊急スレッドも忙しいかもしれません。自分のことができないとタスクキューを管理できません)
拒否戦略: コアスレッドがいっぱい、ブロックキューがいっぱい、緊急スレッドもいっぱい、応急処置救えない、断るしかないよ~

ここに画像の説明を挿入

コードの説明

day02.TestThreadPoolExecutor は、スレッド プールのコア構成をより鮮明な方法で示します。

3. 待機 vs 睡眠

必要とする

  • 違いが分かるようになる

1 つの共通点、3 つの相違点

共通点

  • wait()、wait(long)、sleep(long) の効果は、現在のスレッドが一時的に CPU を使用する権利を放棄し、ブロッキング状態になることです。

違い

  • メソッドの帰属が異なります

    • sleep(long) は Thread の静的メソッドです
    • wait()、wait(long) はすべて Object のメンバー メソッドであり、各オブジェクトには
  • 違う時間に起きる

    • sleep(long) と wait(long) を実行するスレッドは、対応するミリ秒間待機した後にウェイクアップします。
    • wait(long) と wait() は、notify によって起動することもできます。wait() が起動しない場合は、永久に待機します。
    • これらはすべて割り込みとウェイクアップが可能です(対応するスレッド オブジェクトを取得し、その割り込みメソッドを呼び出し、例外をスローしてウェイクアップします)。
  • さまざまなロック特性 (強調)

    • wait メソッドの呼び出しでは、まず wait オブジェクトのロックを取得する必要がありますが、sleep にはそのような制限はありません
    • wait メソッド (同期されたコード ブロック内) は実行後にオブジェクト ロックを解放し、他のスレッドがオブジェクト ロックを取得できるようにします (CPU はあきらめますが、まだ使用できます)。
    • また、同期されたコード ブロックでスリープが実行された場合、オブジェクト ロックは解放されません(CPU を放棄するため、使用できなくなります)。

wait メソッドの呼び出しは、最初に wait オブジェクトのロックを取得する必要があります。wait メソッドは、synchronized によってロックされたコード ブロック内でのみ呼び出すことができます。通常のコード ブロックで wait を呼び出すと、IllegalMonitorStateException例外がスローされます。不正なモニター状態例外

ここに画像の説明を挿入
ここに画像の説明を挿入
これらはすべて中断してウェイクアップすることができます。t1.interrupt() は例外を強制的にスローし、すぐに catch(){...} コード ブロック内のコードを実行します。これも強制ウェイクアップです。

4. ロックと同期

必要とする

  • ロックと同期の違いをマスターする
  • ReentrantLock の公平なロックと不公平なロックを理解する
  • ReentrantLock の条件変数を理解する

3つのレベル

違い

  • 文法レベル
    • synchronized はキーワードであり、ソース コードは jvm であり、C++ 言語で実装されています。
    • ロックはインターフェースであり、ソースコードはjdkによって提供され、java言語で実装されます。
    • synchronizedを使用する場合、終了同期コードブロックのロックは自動的に解放されますが、Lockを使用する場合は、手動でunlockメソッドを呼び出してロックを解放する必要があります。
  • 機能レベル
    • どちらもペシミスティック ロックに属し、基本的な相互排他、同期、およびロックの再エントリ機能を備えています。
    • Lock は、待機状態の取得、Fair Lock、割り込み可能、​​タイムアウト、複数の条件変数など、Synchronized にはない多くの機能を提供します。
    • Lock には、ReentrantLock、ReentrantReadWriteLock など、さまざまなシナリオに適した実装があります。
  • パフォーマンスレベル
    • 競合がない場合synchronized は偏ったロックや軽量ロックなどの多くの最適化を実行しており、パフォーマンスは悪くありません。
    • 一般に、ロックの実装は、競合が多い場合にパフォーマンスが向上します。

synchronized waitとnotifyを使用して同期を達成します
Lock 条件変数(awaitとsignal)を使用して同期を達成しますロックの再入力機能: 同じオブジェクトに複数のロックを追加し、将来複数のロックのロックを解除しますロックはJava言語で実装されています
たとえば、どのスレッドがブロックされているかを確認する場合など、より便利です。Synchronized は C++ で実装されているため、それを見ることができません。Synchronizedは不公平性のみをサポートします (必ずしも先着順で実行される必要はなく、キューにジャンプすることもできます。一般に、キューにジャンプする方が効率的です) 。中断とタイムアウトをサポートします。同期は 1 つの条件変数と 1 つの待機キューのみに相当します。ロックには複数の条件変数と複数の待機キューがありますリエントラント: 再入可能です。ReentrantReadWriteLock は、読み取りが多く書き込みが少ないシナリオに適しています。









フェアロック

  • フェアロックのフェアな具現化
    • ブロッキング キュー内に既に存在するスレッド(タイムアウトに関係なく) は、常に先入れ先出しで公平です。
    • 公平なロックとは、ブロッキング キューにないスレッドがロックをめぐって競合することを指します。キューが空でない場合は、キューの最後まで正直に待ちます。
    • 不公平なロックとは、ブロッキング キューにないスレッドがロックをめぐって競合し、キュー ヘッドによって起動されたスレッドと競合することを意味します。
  • 公平なロックはスループットを低下させるため、通常は使用されません

条件変数

  • ReentrantLock の条件変数の機能は、通常の同期待機と通知に似ています。これは、スレッドがロックを取得し、条件が満たされていないことが判明したときに、リンク リスト構造で一時待機するために使用されます。
  • 同期待機セットとの違いは、ReentrantLock では複数の条件変数を使用できることで、より詳細な待機とウェイクアップの制御を実現できます。

コードの説明

  • day02.TestReentrantLock は、ReentrantLock の内部構造をより鮮明に示します。

Lock メソッドには 2 つのキューがあります。
1. ブロック キュー: ロックをスクランブルしていないスレッドが、このブロッキング キューに 1 つずつ入ります。
2. 待機キュー: ロックをスクランブルしたスレッドは、実行時に条件が満たされていないことがわかります。ロック内のコード (例: 別のスレッドの結果を待機することによって引き起こされる同期の問題)、この時点で、スレッドは conditon1.await() メソッドを積極的に実行して自身をブロックし、この時点で待機キューに入ります。待機する条件変数conditon1。複数の条件変数を使用でき、さまざまな理由でブロックする必要があるスレッドを一時停止するために使用されます。
conditon1.await() セマフォ待ちキューに移動して
conditon1.signal() を待ちます セマフォ待ちキューのスレッド (キューヘッド) をウェイクアップします ウェイクアップ後、ブロックされているキューの最後尾に移動します (同期されたキューはブロックされたキューの先頭に移動)
conditon1.signalAll( ) セマフォをウェイクアップし、キュー内のすべてのスレッド (キュー ヘッド) がウェイクアップ後にブロックされたキューの最後に移動するのを待ちます。

synchronized の最下層は C++ であり、コードのデバッグでは実証できません。最下層の実装はロックの実装と同じではありません。たとえば、セマフォがウェイクアップした後のスレッドは待機キューからキューの先頭に移動します。ブロックされたキュー (優先度が高い)

5.揮発性

必要とする

  • スレッドセーフを習得する際に考慮すべき 3 つの問題
  • volatileを使いこなすことでどのような問題が解決できるのか

原子性

  • 原因: マルチスレッドでは、異なるスレッドの命令がインターリーブされるため、共有変数の読み取りと書き込みの混乱が生じます。
  • 解決策: ペシミスティック ロックまたはオプティミスティック ロックを使用します。volatile ではアトミック性を解決できません。

アトミック性の問題の例は次のとおりです。
ここに画像の説明を挿入
本来、+5 -5 の後の値は 10 のままである必要がありますが、インターリーブ操作により、最終的なバランス = 5 が
解決されます: lock->become atomic
orCAS解決するには (変更する前に CAS=>compareAndSet を実行し、まず変数値が古い値と等しいかどうかを比較します。つまり、他のスレッドによって秘密裏に変更されていないかどうかを確認します。) CAS はアトミック性も保証できます

可視性

  • 原因:他のスレッドには表示されない、コンパイラの最適化、キャッシュの最適化、または CPU 命令の並べ替えの最適化による共有変数の変更
  • 解決策: 共有変数を volatile で修飾すると、コンパイラなどの最適化が行われなくなり、あるスレッドによる共有変数の変更が別のスレッドに見えるようになります。

可視性の問題の例:
ここに画像の説明を挿入
簡単な説明: スレッド 0 スレッドによる stop 共有変数の変更は、メイン スレッドからは認識されません (OS は効率を向上させるために命令の最適化を行っており、速度は速くなりますが、いくつかの疑問も生じます)
ここに画像の説明を挿入

しかし、上記の説明には不足があり、正確には、JIT (JIT (just in time): ジャストインタイムコンパイラ) の最適化によって引き起こされるはずです。JIT は、頻繁に呼び出されるメソッドやループ、つまり頻繁に実行されるものを最適化します
。 、以下の while(!stop){…} メソッドの本体コードは単純すぎるため、0.1 秒で 1,000 万回ループし、毎回メモリ内の stop の値を読み取ることになり、比較すると多すぎます。遅い、JITはそれを見てじっとしていられず、最適化してコンパイル解釈-》の最後の機械語while(!false)を直接キャッシュして、メモリストップの再読み込みをやめたのだと思います。 stop===false なので、停止は変更され、後で見つけることはできません。
もちろん、この最適化は、コードが何度も繰り返し実行され、その回数が JIT 最適化のしきい値を超えたためです。 (500,000 回)、最適化される前
コードの実行数が非常に少ないため、JIT は最適化されません

上記の Thread.sleep(100); は、while(!stop) ループを 1,000 万回実行するのに十分であり、しきい値をはるかに超えています。これを Thread.sleep(1); に変更する
と、無限ループが 1ms 以内に 10,000 回を超えて実行されると、最適化されず、メモリが読み取られ、停止が変更されたことがわかります。表示されない問題が発生しました。
ここに画像の説明を挿入

解決策: volatile で変数を変更すると、その変数は JIT コンパイルと最適化によってキャッシュされなくなり、メモリ内の値を実際に読み取るたびに変更がすぐに確認できるため、この問題は解決されます。(JIT コンパイルの最適化により、プログラムの効率が 10 ~ 100 倍向上しますが、オフにすることはできません)
ここに画像の説明を挿入

秩序

  • 原因:コンパイラの最適化、キャッシュの最適化、または CPU 命令の並べ替えの最適化により、命令の実際の実行順序が書き込み順序と一致しません。
  • 解決策: volatile を使用してシェア変数を変更すると、シェア変数の読み取りおよび書き込み時に異なるバリアが追加され、他の読み取りおよび書き込み操作がバリアを越えることができなくなり、並べ替えを防止する効果が得られます。
  • 知らせ:
    • 揮発性変数の書き込みに追加されるバリアは、バリアを超える他の書き込み操作が揮発性変数の書き込みの下でキューに入れられるのを防ぐためです(前の書き込み命令は正直に最初に書き込まれます (ただし、後者の命令が先に行って最初に書き込むことができます)) =>後に書く
    • volatile 変数 readに追加されるバリアは、以下の他の読み取り操作がバリアを越えてvolatile 変数 readの上に配置されるのを防ぐためです(次の読み取り命令は、私が正直に読み取りを完了するのを待ってから、私の後ろから読み取ります (ただし、目の前にある指示を読んでから実行できます。後で読みます)) = "最初に読んでください
    • 揮発性の読み取りおよび書き込みによって追加されるバリアは、同じスレッド内での命令の並べ替えのみを防止できます。

ここに画像の説明を挿入

したがって、最初に読み取られてから書き込まれる変数に揮発性の変更を追加する必要があります。
ここに画像の説明を挿入
初心者は volatile の使用をお勧めしません

コードの説明

  • day02.threadsafe.AddAndSubtract はアトミック性を示します
  • day02.threadsafe.ForeverLoop デモの可視性
    • 注: この例は、コンパイラの最適化によって引き起こされる可視性の問題であることが証明されています。
  • day02.threadsafe.Reordering は順序付けを示します
    • jar パッケージにパッケージ化してテストする必要がある
  • 動画解説も参考にしてください

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

必要とする

  • 悲観的ロックと楽観的ロックの違いをマスターする

悲観的ロックと楽観的ロックの比較

  • 悲観的なロックの代表的なものは同期され、ロックがロックされます。

    • 中心的な考え方は、[スレッドは、ロックを所有している場合にのみ共有変数を操作できます。毎回 1 つのスレッドだけがロックを正常に占有することができ、ロックの取得に失敗したスレッドは停止して待機する必要があります] です。
    • スレッドの実行からブロック、そしてブロックからウェイクアップまでの間にスレッド コンテキストの切り替えが発生し、これが頻繁に発生するとパフォーマンスに影響します。
    • 実際、スレッドが同期ロックを取得し、ロックがすでに占有されている場合は、ブロックされる可能性を減らすために数回再試行します。
  • 楽観的ロックの代表的なものは AtomicInteger で、これは cas を使用して原子性を保証します。

    • その中心的なアイデアは、[ロックする必要はなく、毎回 1 つのスレッドだけが共有変数を正常に変更でき、失敗した他のスレッドは停止する必要がなく、成功するまで再試行し続ける] です。
    • スレッドは常に実行されているため、ブロックする必要がなく、スレッド コンテキストの切り替えは必要ありません。
    • マルチコア CPU のサポートが必要であり、スレッド数が CPU コアの数を超えてはなりません

典型的なオプティミスティック ロックは AtomicInteger で、AtomicInteger の最下層は Unsafe
ここに画像の説明を挿入
最下層の継続的な試行であり、次のコード原則に似ています
ここに画像の説明を挿入
。compareAndSetXXX => 略語 == CAS == CAS はアトミック性のみを保証し、可視性は保証できないこと
に注意してください( JIT はキャッシュのバイトコードを最適化します - 「マシン コード) の可視性は volatile によって保証されているため、cas が共有変数を操作する場合でも、共有変数は volatile で修飾する必要があります」

ここに画像の説明を挿入

悲観的ロックと楽観的ロックはどのようにして共有変数のスレッドの安全性を確保するのでしょうか?
悲観的ロック: 同期により、一度に 1 つのスレッドのみが共有コード ブロックに入ることができます。つまり、コード ブロック全体がアトミックです。もちろん、共有変数へのアクセスはスレッドセーフです。(簡単に言うと、アトミック性は命令のインターリーブを防ぎ、効率を犠牲にし、安全性を確保します)
ここに画像の説明を挿入
オプティミスティックロック、cas の新旧値の比較により、毎回 1 つのスレッドだけが同じ値を変更できるようになり、変更に失敗したスレッドは保持されます。再試行し、新しい値を再度読み取ってから操作することで、安全性が確保されます。CAS には相互排他やブロックがなく、命令を自由にインターリーブでき、共有変数の正確性は変更前の比較を通じて保証できます。
ここに画像の説明を挿入

コードの説明

  • day02.SyncVsCas は、アトミックな割り当てを解決するための楽観的ロックと悲観的ロックの使用を示します。
  • 動画解説も参考にしてください

まとめ:原子性はロックを追加することによってのみ解決できます。悲観的なロックと楽観的なロックは問題ありません。
可視性: 揮発性によってのみ解決可能、コンパイラ最適化命令の再配置によって生じる変数が変更されても表示されないのを防ぐため
秩序性: 揮発性によってのみ解決できます, コンパイラ最適化命令などの再配置により、命令の実際の実行順序が書き込み順序と不一致になり、予期しない結果が生じることを防ぐためですが、「最初に読み取り、次に書き込み」シェア変数に volatile を追加する必要があります。

7. ハッシュテーブルと同時ハッシュマップ

必要とする

  • Hashtable と ConcurrentHashMap の違いをマスターする
  • さまざまなバージョンにおける ConcurrentHashMap の実装の違いをマスターする

より鮮明なデモについては、データ内の hash-demo.jar を参照してください。操作には jdk14 以降の環境が必要です。jar パッケージ ディレクトリに入り、次のコマンドを実行します。

java -jar --add-exports java.base/jdk.internal.misc=ALL-UNNAMED hash-demo.jar

ハッシュテーブルと同時ハッシュマップ

  • Hashtable と ConcurrentHashMap はどちらもスレッドセーフなMap コレクションです(キーと値を空にすることはできません)。
  • ハッシュテーブルの同時実行性は低く、ハッシュテーブル全体がロックに対応し、同時に操作できるスレッドは 1 つだけです (各展開 n->2*n+1)
  • ConcurrentHashMap は高度な同時実行性を備えています。ConcurrentHashMap 全体が複数のロックに対応しています。スレッドが異なるロックにアクセスする限り、競合は発生しません (ロックの数が同時実行性の程度を決定します)。

同時ハッシュマップ 1.7

  • データ構造: Segment(大数组) + HashEntry(小数组) + 链表、各セグメントはロックに対応し、複数のスレッドが異なるセグメントにアクセスしても競合は発生しません。

(それぞれの大きな配列は小さな配列に対応します。たとえば、容量 Capacity=32、大きな配列の長さ clevel=8 の場合、大きな配列の各要素には、capacity/clevel=32/8=4 のサイズの小さな配列が必要です) (拡張係数の拡張は容量の増加につながり、レベルは変更できないため、最終的には小さなアレイの拡張につながります(小さな配列は、HashMap 内の対応する実際のストレージ配列です。小さな配列が競合する場合、ジッパー化され、ツリー化されます。)
注: おそらく容量 < clevel である可能性があります。 現時点では、小さな配列の最小容量は2です。

  • 同時実行性: セグメント配列のサイズが同時実行性であり、同時にアクセスできるスレッドの数が決まります。Segment 配列は拡張できません。つまり、同時実行性は ConcurrentHashMap の作成時に固定されます。
  • 指数計算
    • 大きな配列の長さが2 m 2^mであると仮定します。2m、大きな配列内のキーのインデックスは、キーのセカンダリ ハッシュ値の上位 m ビットです (ハッシュ(キー) 値を計算し、上位 m ビットを取得し、それを、シリアルナンバー)
    • 小さな配列の長さが2 n 2^nであると仮定します。2n、小さい配列内のキーのインデックスは、キーのセカンダリ ハッシュ値の下位 n ビットです (ハッシュ(キー) 値を計算し、下位 n ビットを取得して、小さい配列の対応する添字に配置します)配列)
  • 拡張:各小さな配列の拡張は比較的独立しています (レベル (同時実行) の小さな配列はそれぞれ拡張されます)。小さな配列が拡張係数を超えると、拡張がトリガーされ、拡張は毎回 2 倍になります (各拡張 n ->2*n)
  • Segment[0] プロトタイプ: 初めて他の小さな配列を作成するとき、このプロトタイプがベースとして使用されます。配列の長さと拡張率はプロトタイプに基づきます。以下に小さな配列はありません
    。空の
    Segment[0] の小さな配列にも通常通り値が格納されますが、これも大きなサイズに拡張されます。このとき、他の新しい Segment が値を 1 つしか持たない場合でも、Segment[ と同じサイズの配列が作成されます。 0] 小さな配列が作成されます (実際には、デザインパターンのプロトタイプパターンです)

ConcurrentHashMap 1.8
(セグメント大きな配列はなく、直接配列+リンクされたリスト/赤黒ツリーです)

  • データ構造: Node 数组 + 链表或红黑树、配列の各ヘッド ノードはロックとして使用され、複数のスレッドによってアクセスされるヘッド ノードが異なる場合、競合は発生しません。初めてヘッド ノードを生成するときに競合が発生した場合は、パフォーマンスをさらに向上させるために、同期の代わりに cas を使用します。
  • 同時実行性: Node 配列のサイズは同時実行性と同じです。1.7 とは異なり、Node 配列は拡張できます(操作が異なるリンク リストの先頭 (つまり、マッピングの競合ではない要素) である限り)。同時に実行することも可能です)
  • 拡張条件: Node 配列が 3/4 でいっぱいになると拡張します( n*扩容因子1.7 に達すると拡張します) (拡張とは、最初に新しい2*n容量の新しい配列を作成し、次にハッシュを再計算して要素を 1 つずつコピーすることです)
  • 拡張ユニット: リンク リストを単位として使用し、リンク リストを後ろから前に移行します。移行が完了したら、古いアレイ ヘッド ノードを ForwardingNode に置き換えます。
  • 拡張時の同時取得
    • ForwardingNode (更新されたチェーンヘッド) に応じて、新しい配列で検索するか古い配列で検索するかを決定し、ブロックしません
    • リンクされたリストの長さが 1 を超える場合、ノードをコピーする必要があります (新しいノードを作成する)、ノードの移行後に次のポインタが変更されるのではないかという懸念があります。
    • リンク リストの最後のいくつかの要素のインデックスが展開後も変更されない場合、ノードをコピーする必要はありません (次へ 関係は変わらない
  • 容量拡張中の同時プット
    • put スレッドが拡張スレッド操作と同じリンク リストである場合、put スレッドはブロックされます。
    • put スレッド操作のリンク リストが移行されていない場合、つまりヘッド ノードが ForwardingNode でない場合は、同時に実行できます。
    • put スレッド操作のリンクされたリストが移行されている場合、つまりヘッド ノードが ForwardingNode である場合、拡張を支援できます ( put 操作はブロックされますが、このスレッドはアイドル状態にはなりません。) の移行操作を手伝ってください。他のノードの移行バー)
  • 1.7 と比較して、遅延初期化です(1.7 は Segment の大きな配列を初期化し、Segment[0] の小さな配列を作成します。1.8 は初期化後に配列を作成せず、遅延スタイルです)
  • 容量は要素の推定数を表し、初期配列サイズを決定するための容量 / ファクトリ、 2 n 2^nを取るには近くにある必要があります2n
  • loadFactor は、初期配列サイズを計算する場合にのみ使用されます。以降、伸張率は3/4固定となります。
  • ツリーのしきい値を超えたときの拡張の問題。容量がすでに 64 の場合は直接ツリーを実行し、それ以外の場合は元の容量に基づいて 3 ラウンドの拡張を実行します。

8. スレッドローカル

必要とする

  • ThreadLocalの機能と原理をマスターする
  • ThreadLocalのメモリ解放タイミングを把握する

効果

  • ThreadLocal は、[リソース オブジェクト] のスレッド分離を実現し、各スレッドが独自の [リソース オブジェクト] を使用できるようにし、競合によって引き起こされるスレッド セーフティの問題を回避できます (まったく逆の考え方により、スレッド セーフティの問題が解決され、共有されず、それぞれが独自のリソースを使用します)。他にもリソースはあります)
  • ThreadLocal は、スレッド内でのリソース共有も実装します(多くのメソッドをスレッド内で実行する必要がある場合があり、メソッドのローカル変数はメソッド間で共有できます)。

ここに画像の説明を挿入
どのスレッドが tl.get() を実行してスレッド自身のローカル変数領域を取得することで自然にスレッド間の分離が実現され、
同じスレッドであればコードのどの部分であっても ThreadLocal の Get メソッドを呼び出します。 () object tltl.get()で取得します このスレッド内で唯一のローカル変数領域であり、スレッド内でのリソース共有を実現します。

原理

各スレッドには ThreadLocalMap タイプのメンバー変数があり、リソース オブジェクトを格納するために使用されます。

  • set メソッドを呼び出すと、ThreadLocal 自体がキーとして、リソース オブジェクトが値として使用され、それが現在のスレッドの ThreadLocalMap コレクションに追加されます。

(ここでの ThreadLocal は、公開キーとして関連付けの役割のみを果たします。そのため、ThreadLocal は、同じタイプのリソースを表す同じオブジェクトにすることができます。したがって、複数の新しい ThreadLocal を複数の共通キーとして使用でき、現在のスレッド (ThreadLocalMap 内) に複数種類の値を格納できます。容量は 16))
ハッシュ値については、最初の ThreadLocal のハッシュ値は 0 で、各 threadLocal.set(xx) の後のハッシュ値は +1640531527 になり、その余りを直接取得して ThreadLocalMap の対応する位置にマッピングします。

  • get メソッドを呼び出すと、ThreadLocal 自体がキーとして使用され、現在のスレッドで関連するリソース値が検索されます。
  • Remove メソッドを呼び出すと、ThreadLocal 自体をキーとして使用して、現在のスレッドに関連付けられたリソース値を削除します。

ThreadLocalMap のいくつかの機能

  • キーのハッシュ値は均一に分散されます
  • 初期容量は16、拡張倍率は2/3、拡張容量は2倍になります
  • キー インデックスが競合した後は、オープン アドレス指定方法を使用して競合を解決します (ThreadLocalMap はジッパー メソッドを使用しません) (オープン アドレス指定方法は、後で次の空き場所を見つけることです) 。

弱い参照キー

ThreadLocalMap のキーは、次の理由から弱い参照として設計されています。

  • スレッドは長時間実行する必要がある場合があります (スレッド プール内のスレッドなど)。キーが使用されなくなった場合は、メモリが不足したときにキーが占有しているメモリを解放する必要があります (GC)。
    これらのキーは今後使用する必要はありませんが、実際にマップ内でキーとして参照されるため、強く参照されると GC によって解放されず、仮想マシンのメモリを常に占有します。
    (ガベージ コレクションでは、弱参照は解放できますが、強参照は解放できません。弱参照として設定すると、自分でメモリを解放し忘れることを防ぐことができます。ガベージ コレクション中に、これらの弱参照はもう誰も使用していないことがわかります (オブジェクトがどの変数からも参照されていない場合、プログラムはこのオブジェクトを使用できなくなります。)、これらのキー オブジェクトは自動的にリサイクルされ、メモリを消費しません)
    (ただし、値は強参照であり、メモリは GC によって解放されません)

メモリ解放時間

  • パッシブ GC がキーを解放する
    • キーのメモリのみが解放され、値に関連付けられたメモリは解放されません。
  • 値を遅延かつ受動的に解放します (値をクリーンアップするために ThreadLocalMap をスキャンしたり、取得/設定時に自分自身または次の値をクリーンアップしたりする必要はありません)。
    • キーを取得する際、それがnullキーであることが判明した場合、その値のメモリは解放されます(通常のマップとは異なり、nullキーにはキーが埋められますが、値はnullに設定されます)。
    • キーを設定するとき、隣接する null キーの値メモリをクリアするためにヒューリスティック スキャンが使用されます。ヒューリスティックの数は、要素の数と null キーが見つかったかどうかに関係します(キーを設定するときに、キーが存在することがわかりました)私の領域はnullで、新しいキーを入力し、値には次の値があります。古い値は確実にクリーンアップされ、そのnull key- 非null value隣の値もクリーンアップされるため、ThreadLocalMapをスキャンすることなく未解放のメモリをクリーンアップできます。 。)
  • アクティブに削除してキーと値を解放します
    • キーと値のメモリは同時に解放され、隣接する null キーの値のメモリもクリアされます
    • これを使用することをお勧めします。これは、一般に ThreadLocal を使用するときに静的変数 (つまり、強参照) として使用されるため、受動的に GC リサイクルに依存することができないからです。

メモリ リーク: ガベージ コレクタはメモリの特定の部分を再利用できません。この現象はメモリ リークと呼ばれます。値
オブジェクトには Entry への参照が 1 つだけあります。弱い参照が使用されている場合、メソッド スタック 2 の実行前に gc が発生する可能性があります。が完了し、値がリサイクルされます。ヌル ポインタ例外が発生しました。? ゆっくりメイクしてね~

9. 国有企業: スレッドとプロセスの違い (新しいバージョンのコースが追加されます)

おすすめ

転載: blog.csdn.net/hza419763578/article/details/130556607