Java 並行プログラミング原則 2 (AQS、ReentrantLock、スレッド プール)

1.AQS:

1.1 AQSとは何ですか?

AQS は、抽象キュー シンクロナイザー、つまり抽象キュー シンクロナイザーであり、本質的には抽象クラスです。

AQS にはコア属性状態があり、その後に二重リンク リストと単一項目リンク リストが続きます。

まず、volatile に基づいて状態を変更し、次に CAS に基づいて状態を変更すると同時に、3 つの主要な特性を保証できます。(アトミック、可視、​​順序付き)

第二に、二重リンクリストも提供されます。Node オブジェクトで構成される二重リンク リストがあります。

最後に、Condition 内部クラスでは、Node オブジェクトで構成される一方向リンク リストも提供されます。

AQS は JUC の多数のツールの基本クラスであり、ロック、CountDownLatch、セマフォ、スレッド プールなど、多くのツールは AQS に基づいて実装されており、すべて AQS を使用しています。


状態とは: 状態は int 型の値、同期状態、状態が何であるかについては、サブクラスの実装に依存します。

条件と一方向リンク リストとは: 同期では wait メソッドと notification メソッドの使用が提供されることはよく知られていますが、lock lock もこのメカニズムを実装する必要があります。lock lock は、AQS 内の条件に基づいて await メソッドと signal メソッドを実装します。(待機と同期の通知用)


スレッドがロックを保持しているときに sync が wait メソッドを実行すると、ウェイクアップを待つためにスレッドを WaitSet 待機プールにスローします。

lcok は、スレッドがロックを保持しているときに await メソッドを実行し、スレッドを Node オブジェクトとしてカプセル化し、Condition 一方向リンク リストにスローしてウェイクアップを待ちます。


Condition の動作: ロックを保持しているスレッドをノードとしてカプセル化し、Condition の一方向リンク リストに投入し、同時にスレッドを一時停止します。スレッドが起動した場合は、条件内のノードを AQS 二重リンク リストにスローし、ロックが取得されるまで待ちます。

1.2 スレッドを起動するときに AQS が後ろから前にトラバースするのはなぜですか?

スレッドがリソースを取得しない場合は、スレッドを Node オブジェクトとしてカプセル化し、AQS 二重リンク リストに配置し、場合によってはスレッドを一時停止する必要があります。

スレッドをウェイクアップするときにヘッド ノードの次のノードが最初にウェイクアップされる場合、ヘッドの次のノードがキャンセルされると、AQS のロジックはテール ノードから順方向にトラバースして、次のノードに最も近い有効なノードを見つけます。頭?

この問題を明確に説明するには、Node オブジェクトがどのように二重リンク リストに追加されるかを理解する必要があります。

addWaiter メソッドに基づいて、最初に現在のノードの prev が末尾ノードを指すようにし、次に末尾が自分自身を指すようにし、次に prev ノードが自分を指すようにします。

下図に示すように、手順 2 のみを実行すると、この時点で AQS キューにノードが追加されますが、現在のノードは前のノードの後に​​見つかりません。

画像.png

1.3 AQS はなぜ双方向リンク リストを使用するのですか (なぜ一方向リンク リストを使用しないのですか)?

AQSにはノードをキャンセルする操作があるため、ノードをキャンセルした後はAQSの二重リンクリストから切り離す必要があります。

二重リンクリストの整合性を確保することも必要です。

  • 前のノードの次のポインタは次のノードを指す必要があります。
  • 次のノードの前ポインタは、前のノードを指す必要があります。

通常の二重リンクリストであれば直接操作できます。

ただし、一方向リンク リストの場合は、上記の操作を完了するために一方向リンク リスト全体を走査する必要があります。それは資源の無駄遣いです。

1.4 AQS に仮想ヘッド ノードがあるのはなぜですか

センチネルノードがあり、操作がより便利になります。

もう 1 つは、AQS 内部で各ノードが何らかの状態を持ち、この状態はそれ自体だけでなく後続のノードにも適用されるためです。

  • 1: 現在のノードがキャンセルされます。
  • 0: デフォルト状態。何も起こりません。
  • -1: 現在のノードの後継ノード、保留中。
  • -2: 現在のノードが条件キュー内にあることを表します (await はスレッドを一時停止します)。
  • -3: 現在共有ロックであることを意味します。ウェイクアップする場合、後続のノードもウェイクアップする必要があります。

Node ノードの ws は多くの情報を表し、現在のノードの状態に加えて、後継ノードの状態も維持します。

仮想ヘッドノードがキャンセルされた場合、ノードは現段階の状態と後続ノードの状態を同時に保存することができません。

同時に、ロック リソースを解放するときは、ヘッド ノードの状態が -1 であるかどうかに基づいて行う必要があります。後続ノードを起動するかどうかを決定します。

-1 の場合、通常通り起動します

-1 ではない場合、ウェイクアップする必要がありますか?これにより、可能性のあるトラバーサル操作が減り、パフォーマンスが向上します。

1.5 ReentrantLock の基本的な実装原理

ReentrantLock は AQS に基づいて実装されます。

ReentrantLockでスレッドをロックする場合はCASに基づいてstate属性を変更する必要があり、0から1に変更できればロックリソースの獲得に成功したことになります。

CAS が失敗した場合は、AQS の二重リンク リストとキューに追加され (スレッドが一時停止される可能性があります)、ロックの取得を待機します。

ロックを保持しているスレッドが条件の await メソッドを実行すると、スレッドはノードとしてカプセル化され、条件の一方向リンク リストに追加され、目覚めるのを待ってロック リソースを再競合します。

Javaでは、後述するスレッドプール内のWorkerロックを除き、すべてリエントラントロックとなります。

1.6 ReentrantLockの公平ロックと不公平ロックの違い

  • 公平なロックと不公平なロックにおける lock メソッドと tryAcquire メソッドの実装には内部的な違いが 1 つあり、その他は同じです。
    • 不公平なロック lock: 状態を 0 から 1 に直接変更しようとします。成功した場合は、ロックを取得して直接進み、失敗した場合は、tryAcquire を実行します。
    • Fair lock lock: tryAcquire を直接実行します。
    • 不公平なロック tryAcquire: 現在ロック リソースを保持しているスレッドがない場合は、状態を 0 から 1 に変更することを再試行します。成功した場合は、ロックを取得して直接実行します。
    • 公平なロック tryAcquire: 現在ロック リソースを保持しているスレッドがない場合は、まずキューがあるかどうかを確認します。
      • キューがない場合は、状態を 0 から 1 に直接変更してみます。
      • 列ができていても、私は一番前ではないので、焦らずに待ち続けてください。
      • キューがある場合は、私が最初で、状態を 0 から 1 に直接変更しようとします。
    • ロックが取得されなかった場合、フェアロックとアンアンフェアロックの追従ロジックは同じであり、キューイング後のいわゆるキュージャンプはありません。

実例: 不公平なロックには、強制的にロック リソースを 2 回取得しようとする機会があり、成功した場合は喜んで終了し、失敗した場合は消滅してキューに入れられます。

  • 誰かが核酸をやりに来た
    • フェアロック: 最初に見て、キューがある場合はキューに移動します
    • アンフェアロック:どんな状況であっても、最初はスツールの上で行うようにしてください。座ると直接拘束され、椅子にたどり着けない場合は拘束後に退場します。
      • 誰かが喉をつまんでいませんか?
        • 誰もバックルを締められていません。上がって大便をしてみてください。成功した場合は減点後に退場します。
        • 誰かが座っていたら、立ち止まって列に並びましょう。

1.7 ReentrantReadWriteLock による読み取り/書き込みロックの実装方法

書き込み量が少なく読み取り量が多く、ミューテックスを使用する操作の場合、読み取りと読み取りの同時実行性の問題がないため、パフォーマンスが低すぎます。

それを解決する方法は、読み取り/書き込みロックがあります。

ReentrantReadWriteLock も AQS に基づく読み取り/書き込みロックですが、ロック リソースは状態によって識別されます。

int に基づいて 2 つのロック情報を識別するには、書き込みロックと読み取りロックがありますが、どうすればよいですか?

int は 32 ビットを占有します。

書き込みロックがロックを取得すると、状態の下位 16 ビットの値が CAS に基づいて変更されます。

読み取りロックがロックを取得すると、状態の上位 16 ビットの値が CAS に基づいて変更されます。

書き込みロックは相互に排他的であるため、書き込みロックの再エントリは状態の下位 16 に基づいて直接識別されます。

読み取りロックは共有されており、複数のスレッドによって同時に保持される可能性があるため、読み取りロックの再入力は状態の上位 16 ビットに基づいて識別できません。したがって、読み取りロックの再エントリは ThreadLocal によって表され、同時に状態の上位 16 が追加されます。

2. キューをブロックする高頻度の問題:

2.1 キューのブロッキング

ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue

ArrayBlockingQueue: 最下層は配列に基づいて実装されます。新規作成時に境界を設定することを忘れないでください。

LinkedBlockingQueue: 最下層はリンク リストに基づいて実装され、無制限のキューと見なされますが、長さを設定できます。

PriorityBlockingQueue: 最下層は配列に基づいて実装されたバイナリ ヒープです。配列は拡張されるため、無制限のキューとみなすことができます。

ArrayBlockingQueue と LinkedBlockingQueue は、ThreadPoolExecutor スレッド プールで最も一般的に使用される 2 つのブロッキング キューです。

PriorityBlockingQueue: ScheduleThreadPoolExecutor タイミング タスク スレッド プールによって使用されるブロッキング キューは、PriorityBlockingQueue の基礎となる実装と同じです。(実は本質はDelayWorkQueueです)

2.2 誤ったウェイクアップ

誤ったウェイクアップは、ブロッキング キューのソース コードに反映されます。

たとえば、コンシューマ 1 がデータを消費するとき、最初にキューに要素があるかどうかを確認し、要素の数が 0 の場合、コンシューマ 1 はハングアップします。

ここで要素が0と判定される位置はifループを使うと問題になります。

プロデューサがデータを追加すると、コンシューマ 1 が起動されます。

ただし、コンシューマ 1 がロック リソースを取得できない場合、コンシューマ 2 がロック リソースを取得し、データを奪います。

コンシューマ 1 がロック リソースを再度取得すると、キューから要素を取得できなくなります。論理的な問題を引き起こします。

解決策は、要素数を判定する位置を判定中に設定することです。

3. スレッドプール

3.1 スレッドプールの7つのパラメータ(帰宅時には通知されません)

コアスレッドの数、スレッドの最大数、最大アイドル時間、時間単位、ブロッキングキュー、スレッドファクトリー、拒否ポリシー

3.2 スレッド プールの状態はどのようなものですか?また、それはどのように記録されますか?

スレッド プールは常にアクティブであるとは限りません。
スレッド プールには 5 つの状態があります。
画像.png

スレッド プールの状態は ctl 属性に記録されます。本質はint型です
画像.png

ctl の上位 3 ビットは、スレッド プールの状態を記録します。

下位 29 ビットにはワーカー スレッドの数が記録されます。指定したスレッドの最大数が Integer.MAX_VALUE であっても、それに到達することはできません

3.3 スレッド プールの一般的な拒否戦略 (ホームに戻ることに関する通知なし)

AbortPolicy: 例外をスローします (デフォルト)

画像.png

CallerRunsPolicy、タスクの送信者と実行者。非同期から同期へ

画像.png

DiscardPolicy: タスクは直接

画像.png

DiscardOldestPolicy: 最も古いタスクを失い、現在のタスクを処理のためにスレッド プールに引き渡して再試行します。

画像.png

一般に、組み込みのスレッド プールがビジネスを満足できない場合は、スレッド プールの拒否ポリシーを定義します。

次のインターフェースを実装するだけです。

画像.png

3.4 スレッドプールの実行処理(帰宅通知なし)

コア スレッドは新規作成後に構築されません。これは遅延読み込みメカニズムであり、コア スレッドはタスクを追加した後にのみ構築されます。

2 コア スレッド 5 スレッド ブロッキング キューの最大長は 2

画像.png

3.5 スレッド プールが空のタスクの非コア スレッドを追加するのはなぜですか

画像.png

ワークキュー内にタスクがあるものの、処理するワーカー スレッドがないスレッド プールを避けてください。

スレッド プールはコア スレッドの数を 0 に設定できます。このようにして、タスクはブロッキング キューにスローされますが、ワーカー スレッドは存在しません。これはクールですね~~

スレッド プール内のコア スレッドは、リサイクルされないことが保証されません。スレッド プールにはプロパティがあります。これが true に設定されている場合、コア スレッドも強制終了されます。

画像.png

3.6 タスクがないとき、スレッド プール内のワーカー スレッドは何をしているのですか?

スレッドはハングし、デフォルトのコア スレッドは WAITING 状態、非コア スレッドは TIMED_WAITING になります。

コアスレッドの場合、デフォルトではタスクが取得されるまでブロッキングキューの位置で take メソッドが実行されます。

非コア スレッドの場合、デフォルトでは、ポーリング メソッドはブロッキング キューの位置で実行され、最大アイドル時間まで待機します。タスクがない場合は直接引き出され、タスクがある場合は直接引き出されます。動作する場合は、正常に動作します。

3.7 ワーカースレッドの例外によってどのような問題が発生しますか?

例外をスローするか、他のスレッドに影響を与えるか、ワーカー スレッドがスラムするか?

タスクがexecuteメソッドによって実行される場合、ワーカースレッドは例外をスローします。

タスクが submit メソッドによって実行される futureTask の場合、ワーカー スレッドは例外をキャプチャして FutureTask に保存し、futureTask の get に基づいて例外情報を取得できます。

異常なワーカー スレッドは他のワーカー スレッドに影響を与えません。

runWorker の例外が run メソッドにスローされ、run メソッドが異常終了し、run メソッドが終了するとスレッドが停止します。

送信され、例外がスローされない場合は、no ga~

3.8 AQS を継承するワーカー スレッドの目的は何ですか?

ワーカー スレッドの本質は Worker オブジェクトです

AQS の継承は、shutdown と shutdownNow に関連します。

シャットダウンの場合は、アイドル状態のワーカー スレッドを中断し、ワーカーが実装する AQS の状態値に基づいてワーカー スレッドを中断できるかどうかを判断します。

ワーカー スレッドの状態が 0 の場合は、アイドル状態で中断できることを意味し、1 の場合は動作中であることを意味します。

shutdownNow の場合は、すべてのワーカー スレッドを直接強制的に中断します

3.9 コアパラメータを設定するにはどうすればよいですか?

スレッド プールの目的は、CPU リソースを最大限に活用することです。システム全体のパフォーマンスを向上させます。

システム内の業務ごとにスレッドプールの参照方法も異なります。

CPU を大量に使用するタスクの場合、通常は CPU コア数 + 1 のコア スレッド数となります。これは、CPU のパフォーマンスを最大限に発揮するのに十分です。

IO集中型のタスクの場合、IOの程度が1秒のもの、1ミリ秒のもの、1分のものなど異なるため、IO集中型のタスクがスレッドプールで処理される場合は、次のことを観察する必要があります。負荷テストによる CPU リソースの占有率により、コア スレッドの数が決定されます。一般的にはCPU性能を70~80まで出せば十分です。したがって、スレッド プールのパラメータ設定を具体的にするには、圧力テストと複数の調整を行う必要があります。

たとえば、企業は 3 つのサービスをクエリする必要があります。

おすすめ

転載: blog.csdn.net/lx9876lx/article/details/129112863