メインコンテンツまでスキップ

JVM スレッドプールの拡張メカニズム

· 約4分

HotSpot VM のスレッドモデル では、Java スレッドとオペレーティングシステムのスレッドは 1 対 1 のマッピング関係にあります(JDK19 以降で仮想スレッドが導入されましたが、従来のスレッドは依然としてこのモデルです)。つまり、1 本の Java スレッドは 1 本の OS カーネルスレッド(KLT / LWP)に対応します。

Java がスレッドを作成するとき、OS カーネルの API を呼び出して対応するカーネルスレッドを作成する必要があり、OS はそのためにスタック空間やスケジューリング情報などのリソースを割り当てなければなりません。Java スレッドが終了すると、対応するカーネルスレッドも回収されます。したがって スレッドの作成と破棄のコストは高く、スレッド数も無限に増やすことはできません

スレッド数が多すぎると:

  1. スレッド作成が大きなシステムオーバーヘッドをもたらす
  2. CPU が複数のスレッド間で頻繁に コンテキストスイッチ(Context Switch) を行う
  3. 大量のスレッドが システム性能の低下、さらには OOM を引き起こす可能性がある

そのため、実際の開発では通常 スレッドプール(ThreadPoolExecutor) を使ってスレッドのライフサイクルを統一的に管理します。

ThreadPoolExecutor のコアパラメータ

パラメータ説明
corePoolSizeintコアスレッド数。スレッドプール内に長期的に保持されるスレッドの数
maximumPoolSizeint最大スレッド数。タスクキューが満杯のときに作成を許可する最大スレッド数
keepAliveTimelong非コアスレッドの最大アイドル生存時間
unitTimeUnitkeepAliveTime の時間単位
workQueueBlockingQueueタスクキュー。実行待ちのタスクを格納する
threadFactoryThreadFactoryスレッド作成ファクトリ。スレッド名や優先度などのカスタマイズに使う
handlerRejectedExecutionHandler拒否ポリシー。タスクを実行できないときの処理方法

スレッドプールのタスク処理フロー

新しいタスクがスレッドプールに提出されると、スレッドプールは以下の順序で処理します。

1)まずコアスレッドでタスクを実行

現在の実行スレッド数が corePoolSize より小さい 場合、スレッドプールは直接新しいスレッドを作成してタスクを実行します。

2)コアスレッドが満杯 → タスクはキューへ

現在のスレッド数が すでに corePoolSize に達している場合、新しいタスクはすぐにはスレッド作成をトリガーせず、BlockingQueueタスクキュー に入って実行を待ちます。

3)キューが満杯 → スレッドプールを拡張

タスクキューが最大容量に達しており、かつ現在のスレッド数が maximumPoolSize より小さい 場合、スレッドプールは 新しいスレッドを作成してタスクを処理 します。これらのスレッドは 非コアスレッド と呼ばれます。

4)最大スレッド数を超過 → 拒否ポリシー

キューが満杯で、かつ現在のスレッド数がすでに maximumPoolSize に達している場合、スレッドプールは RejectedExecutionHandler 拒否ポリシー を発動します。

主なポリシー:

  • AbortPolicy(デフォルト、そのまま例外をスロー)
  • CallerRunsPolicy
  • DiscardPolicy
  • DiscardOldestPolicy

keepAliveTime の役割

スレッドプール内の 非コアスレッドkeepAliveTime の時間内に 1 つもタスクを実行しなかった 場合、そのスレッドは破棄されます。つまりコアスレッドはデフォルトでは破棄されず、非コアスレッドはアイドル時間が keepAliveTime を超えると回収されます。これによりスレッドプールは、ピーク時に拡張し、低負荷時にスレッドを回収する ことができます。

非常にハマりやすい落とし穴

workQueue無界キュー(LinkedBlockingQueue のデフォルトコンストラクタなど) を使うと、タスクは延々とキューに入り続け、キューはほぼ満杯になりません。その結果、スレッドプールは永遠に拡張のステップまで到達せず、maximumPoolSizekeepAliveTime基本的に効きません。これは多くの本番システムで スレッドプールの設定が無効になる よくある原因でもあります。

まとめ

スレッドプールの拡張経路は、コアスレッド → タスクキュー → 非コアスレッド → 拒否ポリシー、と要約できます。maximumPoolSize はキューが満杯になって初めて効き、keepAliveTime は非コアスレッドの回収タイミングを決めます。スレッドプールを設定する際は、キューの種類の選択がスレッド数のパラメータと同じくらい重要です。無界キューは拡張と拒否ポリシーを有名無実化してしまいます。この一連のチェーン全体を理解すれば、本番で起きる「スレッドプールのパラメータが効かない」という問題の大半に答えが見つかります。

COMMENTS