インタビュアー: スレッド プール内の冗長スレッドはどのように回復されますか?

 
  
 
  
您好,我是路人,更多优质文章见个人博客:http://itsoku.com

最近、JDK のスレッド プール ThreadPoolExecutor のソース コードを読み、スレッド プール内のタスクを実行するプロセスについては大体理解しましたが、実はこのプロセスも非常に理解しやすいので、繰り返しません。他の人は私よりもはるかに上手にそれを書きました。

ただし、私はスレッド プールがワーカー スレッドをどのようにリサイクルするかにもっと興味があるため、スレッド プールについての理解を深めるために簡単に分析しました。

そこで、JDK1.8を例に挙げてみましょう。

1. runWorker(ワーカーw)

ワーカー スレッドが開始されると、runWorker(Worker w) メソッドに入ります。

whileループ内で、タスクが空かどうかをループで判断し、空でない場合はタスクを実行し、タスクが取得できない場合や例外が発生した場合はループを抜けてprocessWorkerExit(w, completedAbruptly)を実行するメソッドです。ワーカー スレッドは削除するために移動されます。

タスクをフェッチするソースは 2 つあります。1 つは firstTask で、ワーカー スレッドが初めて実行されるときに実行されるタスクです。これは最大 1 回しか実行できず、タスクは後で getTask() メソッドからフェッチする必要があります。getTask() がキーのようですが、例外を考慮しないシナリオでは、null を返すということは、ループを抜けてスレッドを終了することを意味します。次に、どのような状況で getTask() が null を返すかを確認する必要があります。

(スペースが限られており、セクションごとに区切られており、途中のタスクを実行する手順は省略されています)

c62655dcb79f52d0406b4d30cfe6674b.png

21672e0f75019fe7a876f3f62d45a489.png

2. getTask() は null を返します

null が返される状況は 2 つあります。赤いボックスを参照してください。

最初のケースでは、スレッド プールの状態はすでに STOP、TIDYING、TERMINATED、または SHUTDOWN であり、ワーク キューは空です。

2 番目のケースでは、ワーカー スレッドの数が最大スレッド数を超えているか、現在のワーカー スレッドがタイムアウトし、他のワーカー スレッドが存在するか、タスク キューが空です。これは理解するのがさらに難しく、要するに、最初に覚えて、後で使用します。

以下、条件 1 と条件 2 は、2 つの場合の判定条件をそれぞれ表すために使用されます。

d6e39855f6c58fe8f35fe4091f7e3804.png

3. ワーカースレッドをリサイクルするシナリオ分析スレッドプール

3.1 shutdown() が呼び出されず、すべてのタスクが RUNNING 状態で完了するシナリオ

このシナリオでは、ワーカー スレッドの数はコア スレッドの数のサイズまで減ります (超過しない場合、リサイクルする必要はありません)。

たとえば、スレッド プールでは、コア スレッドの数は 4 で、スレッドの最大数は 8 です。最初は 4 つのワーカー スレッドがありますが、タスク キューがいっぱいになったら、ワーカー スレッドを 8 つに増やす必要があります。後続のタスクがほぼ実行され、スレッドがタスクを取得できない場合は、状態にリサイクルされます。 (allowCoreThreadTimeOut の値に応じて、ここではデフォルト値が false の場合、つまりコア スレッドがタイムアウトしない場合について説明します。true の場合、ワーカー スレッドはすべて破棄できます)。

前述の条件 1 は最初に除外できます。スレッド プールの状態がすでに STOP、TIDYING、TERMINATED、または SHUTDOWN であり、ワーク キューが空です。スレッド プールは常に RUNNING であるため、この判定は常に false になります。このシナリオでは、条件 1 は存在しないと想定できます。

以下では、タスクが取り出せない場合にスレッドがどのように動作するかを解析します。

step1. タスクキューからタスクを取得するには2つの方法があり、タイムアウト待ちを永久にブロックすることもできます。決定要因は時間変数です。変数には以前に値が割り当てられます。現在のスレッド数がコア スレッドの数より大きい場合、変数 timed は true になり、それ以外の場合は false になります (前述したように、ここでは、allowCoreThreadTimeOut が false の場合のみ説明します)。明らかに、現在議論されているのは、timed が true の場合です。keepAliveTime は通常は設定されず、デフォルト値は 0 なので、基本的にはノンブロッキングとみなされ、タスクを取得した結果がすぐに返されます。

スレッドがウェイクアップを長時間待った後、タスクを取り出すことができないことがわかり、timeOut が true になり、次のサイクルに入ります。

step2.条件 1の判定では、スレッドプールは常に RUNNING であり、コードブロックには入りません。

step3.条件 2の判定に移ります このときタスクキューは空で条件は true CAS によりスレッド数が削減されます 成功した場合は null を返し、失敗した場合は step1 を繰り返します

ここで注意したいのは、複数のスレッドが同時に条件2の判定を通過する可能性があるということですが、予想されるコアスレッド数ではなく、スレッド数が減ってしまうのでしょうか?

たとえば、現在のスレッド数は 5 だけですが、このとき同時に 2 つのスレッドが起動します。条件 2 の判定後、同時に数を減らしていくと、残りのスレッド数は 3 つだけになります。それは期待と矛盾します。

実は違う。このような状況を防ぐために、compareAndDecrementWorkerCount(c) では CAS メソッドを使用し、CAS が失敗した場合は継続して次のサイクルに入り、再判定します。

上の例のように、スレッドの 1 つが CAS に失敗し、ループに再度入ると、ワーカー スレッドの数が 4 つだけで、timed が false であることがわかり、このスレッドは破棄されず、永久にブロックされる可能性があります ( workQueue.take())。

私はこの答えにたどり着くまでに長い間考えてきましたが、ロックなしでコア スレッドの数を確実に再利用できるようにするにはどうすればよいかを考えてきました。CASの謎が判明しました。

ここからも、コアスレッドは存在するものの、そのスレッドはコアか非コアかを区別しておらず、先にコアが作成されるのではなく、コアスレッド数を超えた後に非コアが作成されることがわかります。最終的にどのスレッドが保持されるかは完全にランダムです。

3.2 shutdown() の呼び出し、すべてのタスクが実行されるシーン

このシナリオでは、コア スレッドであっても非コア スレッドであっても、すべてのワーカー スレッドが破棄されます。

shutdown() を呼び出した後、アイドル状態のすべてのワーカー スレッドに割り込み信号が送信されます。

4e4977533ec4afbf5b12bbbcf97d2aec.png

最後に false を渡して、次のメソッドを呼び出します。

4a7d03463494お父さん776ab698c21369476.png

割り込み信号を送信する前に割り込みがあったかどうかを判定し、ワーカースレッドの排他ロックを取得していることがわかります。

割り込みシグナルが発行されるとき、ワーカー スレッドは getTask() でタスクを取得する準備をしているか、タスクを実行しているため、現在のタスクの実行が終了するまで割り込みシグナルは発行されません。ワーカー スレッドがタスクを実行しているときにタスクをロックします。ワーカー スレッドはタスクを実行した後、再び getTask() に進みます。

したがって、getTask() で割り込み例外を処理する方法を確認する必要があるだけです。

d7e12bef48dd29171031480c844456e5.png

getTask() のワーカー スレッドには 2 つの可能性があります。

3.2.1 タスクはすべて完了し、スレッドはブロックされて待機しています。

非常に単純で、割り込み信号によって起動され、次のサイクルに入ります。条件 1に達すると、条件が満たされるとワーカー スレッドの数が減り、null が返され、外側の層がこのスレッドを終了します。

ここでの decrementWorkerCount() はスピン型であり、確実に 1 ずつデクリメントされます。

42e5c5265dc95d1833275fbc3d8ca4b1.png

3.2.2 タスクが完全に実行されていない

shutdown() を呼び出した後、プールを終了する前に、未完了のタスクを実行する必要があります。したがって、現時点ではスレッドがまだ動作している可能性があります。

議論する段階は 2 つあります

フェーズ 1 には多くのタスクがあり、ワーカー スレッドはタスクを取得できます

これにはスレッドの終了は関係しません。スキップして、割り込み信号を受信した後のスレッドのパフォーマンスを分析するだけで済みます。

getTask() を通じてタスクを取得しているスレッド A があるとします。このとき、A に割り込みが発生し、タスクを取得する際に、poll() であれ take() であれ、割り込み例外がスローされます。例外はキャッチされ、次のサイクルに再び入ります。キューが空でない限り、タスクは引き続きフェッチできます。

スレッド A が中断され、タスクを再度フェッチし、workQueue.poll() または workQueue.take() を呼び出した場合、例外はスローされませんか? タスクは正常に取得できますか?

workQueueの実装に依存します。workQueue は BlockingQueue 型で、一般的な LinkedBlockingQueue と ArrayBlockingQueue を例に挙げると、ロック時に lockInterruptibly() が呼び出され、割り込みに応答します。このメソッドは、AQS のacquireInterruptibly(int arg)を呼び出します。

acquireInterruptibly(int arg)は、入り口で割り込み例外を判定する場合でも、parkAndCheckInterrupt()メソッドでブロックする場合でも、割り込みで起こされて割り込み例外を判定する場合は、Thread.interrupted()を使用します。このメソッドはスレッドの割り込みステータスを返し、割り込みステータスをリセットします。言い換えれば、スレッドは中断状態ではなくなるため、タスクが再度フェッチされてもエラーは報告されません。

したがって、これはタスクをフェッチする準備をしているスレッドのサイクルを無駄にしていることに相当します。これはスレッド中断の副作用である可能性があります。もちろん、全体の動作には影響しません。

この点を分析してみると、BlockingQueue はここで割り込み状態をリセットしているだけなのですが、どうやってこのような素晴らしい設計を思いついたのか、とため息しか出ません。ダグ・リー神Orz。

72279a1aac66edc91fe80d21641b43a1.png

a8f67116757a7f2ad68f4a8f10023e7e.png

フェーズ 2 ミッションはもうすぐ終了します

この時点で、タスクはほぼ取得されています。たとえば、ワーカー スレッドが 4 つあり、タスクが 2 つだけ残っている場合、2 つのスレッドがタスクを取得し、2 つのスレッドがブロックされる可能性があります。

タスクを取得する前の判定がロックされていないため、すべてのスレッドが前の検証を通過してworkQueueがタスクを取得する場所に来て、タスクキューが空ですべてのスレッドがブロックされることが起こるのでしょうか?shutdown() が実行されたため、スレッドに割り込み信号を送信できなくなり、スレッドはブロックされ、リサイクルできなくなります。

そんなことは起こらないでしょう。

ワーカースレッド A、B、C、D の 4 つがあり、条件 1条件 2の判定を同時に通過し、タスクをフェッチするところまで来たとします。この場合、ワーク キューには少なくとも 1 つのタスクが存在し、少なくとも 1 つのスレッドがそのタスクを取得できます。

A と B がタスクを取得し、C と D がブロックされたとします。

A、B 次の手順は次のとおりです。

step1. タスクの実行完了後、再度 getTask() を実行すると、条件 1が満たされ、null が返され、スレッドが再利用可能になります。

step2.processWorkerExit(Worker w, boolean completedAbruptly) スレッドをリサイクルします。

リサイクルはスレッドを殺すのと同じくらい簡単ですか? processWorkerExit(Worker w, boolean completedAbruptly) メソッドを見てみましょう。

b0ddb83eba6b34c5536fe7e62bc5e781.png

ご覧のとおり、workers.remove(w) による行の削除に加えて、tryTerminate() も呼び出されます。

b7f3dd8fa570dbd7c303ad0513ddd9bf.png


最初の判定条件はサブ条件を満たしていないため、スキップします。2 番目の条件は、ワーカー スレッドがまだ存在し、アイドル スレッドにランダムに割り込むことです。

ここで問題が発生します。アイドル状態のスレッドを中断しても、ブロックしているスレッドを中断することにはなりません。A と B が同時に終了した場合、A が B に割り込み、B が A に割り込み、AB が相互に割り込み、その結果、ブロックされたスレッドに割り込んでウェイクアップするスレッドが存在しないという可能性はありますか?

答えはやはり、考えすぎです...

A がここに来ることができると仮定すると、A がワーカー スレッド コレクション ワーカーから削除されたことを意味します (tryTerminate() の前に processWorkerExit(Worker w, boolean completedAbruptly) が削除されています)。するとAがBに割り込み、Bが割り込みに来て、作業員の中にAが見つからなくなる。

c53b0d7595cee6afa3fc0cafd842e643.png


つまり、終了するスレッドは互いに割り込むことはできません。私がコレクションを終了した後、私はあなたに割り込みますが、あなたは私に割り込むことはできません。私はすでにコレクションを終了しており、あなたは他のスレッドに割り込むことしかできません。この場合、N 個のスレッドが同時に終了したとしても、少なくとも最後には、残りのブロックされたスレッドに割り込むスレッドが 1 つ存在します。

ドミノのように、割り込み信号は伝播されます。

ブロックされた C および D のいずれかが中断されてウェイクアップされると、ステップ 1 のアクションが繰り返され、ブロックされたすべてのスレッドが中断されてウェイクアップされるまで、サイクルが繰り返し開始されます。

これが、tryTerminate() で false を渡す場合、アイドル状態のスレッドのみを中断する必要がある理由です。

そう思うと、改めてダグ・リーに対する憧れ(広東語)を感じます。デザインも良くできています。

4. まとめ

ThreadPoolExecutor はワーカー スレッドをリサイクルし、スレッド getTask() が null を返した場合はリサイクルされます。

シナリオは 2 つあります。

  1. shutdown() が呼び出されず、すべてのタスクが RUNNING 状態で実行されるシナリオ

スレッドの数が corePoolSize より大きい場合、スレッドはタイムアウトによってブロックされます。タイムアウトが解除されると、CAS は作業スレッドの数を減らします。CAS が成功すると、null が返され、スレッドはリサイクルされます。それ以外の場合は、次のサイクルに入ります。ワーカー スレッドの数が corePoolSize 以下の場合、常にブロックされる可能性があります。

  1. shutdown() を呼び出し、すべてのタスクが実行されるシーン

shutdown() はすべてのスレッドに割り込みシグナルを送信します。これには 2 つの可能性があります。

2.1) すべてのスレッドがブロックされています

割り込みが起動してループに入り、すべての最初の if 判定条件が満たされ、null が返され、すべてのスレッドがリサイクルされます。

2.2) タスクが完全に実行されていない

少なくとも 1 つのスレッドがリサイクルされます。processWorkerExit(Worker w, boolean completedAbruptly) メソッドでは、tryTerminate() が呼び出され、アイドル状態のスレッドに割り込み信号が送信されます。ブロックされたすべてのスレッドは、最終的には 1 つずつ起動されてリサイクルされます。

今回の分析は、昨夜から書き始めて、途中で行き詰まって、今朝も書き続けたので、ブログを書くのに2+2=4時間、考えるのに1時間くらいかかりました。

正直に言うと、まだ少し混乱していて、一度に理解することはできませんし、正しく理解できているかどうかもわかりません。

役に立つかどうかは分かりませんが、スレッドプールへの理解が深まったとしか言いようがありません(自分を慰めています)、そして設計の繊細さも感じました。

もっと良い記事を

  1. Java 高同時実行性シリーズ (全 34 記事)

  2. MySqlマスターシリーズ(全27記事)

  3. Maven マスター シリーズ (全 10 件)

  4. Mybatisシリーズ(全12記事)

  5. データベースとキャッシュの整合性の一般的な実装について話す

  6. インターフェイスの冪等性は非常に重要ですが、それは何ですか? それを達成するにはどうすればよいでしょうか?

  7. ジェネリックは少し難しく、多くの人を混乱させるでしょう。それはあなたがこの記事を読んでいないからです。

↓↓↓ 点击阅读原文,直达个人博客
你在看吗

おすすめ

転載: blog.csdn.net/likun557/article/details/131971123