JVM スレッドプールの拡張メカニズム
HotSpot VM のスレッドモデル では、Java スレッドとオペレーティングシステムのスレッドは 1 対 1 のマッピング関係にあります(JDK19 以降で仮想スレッドが導入されましたが、従来のスレッドは依然としてこのモデルです)。つまり、1 本の Java スレッドは 1 本の OS カーネルスレッド(KLT / LWP)に対応します。
Java がスレッドを作成するとき、OS カーネルの API を呼び出して対応するカーネルスレッドを作成する必要があり、OS はそのためにスタック空間やスケジューリング情報などのリソースを割り当てなければなりません。Java スレッドが終了すると、対応するカーネルスレッドも回収されます。したがって スレッドの作成と破棄のコストは高く、スレッド数も無限に増やすことはできません。
スレッド数が多すぎると:
- スレッド作成が大きなシステムオーバーヘッドをもたらす
- CPU が複数のスレッド間で頻繁に コンテキストスイッチ(Context Switch) を行う
- 大量のスレッドが システム性能の低下、さらには OOM を引き起こす可能性がある
そのため、実際の開発では通常 スレッドプール(ThreadPoolExecutor) を使ってスレッドのライフサイクルを統一的に管理します。
ThreadPoolExecutor のコアパラメータ
| パラメータ | 型 | 説明 |
|---|---|---|
| corePoolSize | int | コアスレッド数。スレッドプール内に長期的に保持されるスレッドの数 |
| maximumPoolSize | int | 最大スレッド数。タスクキューが満杯のときに作成を許可する最大スレッド数 |
| keepAliveTime | long | 非コアスレッドの最大アイドル生存時間 |
| unit | TimeUnit | keepAliveTime の時間単位 |
| workQueue | BlockingQueue | タスクキュー。実行待ちのタスクを格納する |
| threadFactory | ThreadFactory | スレッド作成ファクトリ。スレッド名や優先度などのカスタマイズに使う |
| handler | RejectedExecutionHandler | 拒否ポリシー。タスクを実行できないときの処理方法 |
スレッドプールのタスク処理フロー
新しいタスクがスレッドプールに提出されると、スレッドプールは以下の順序で処理します。
1)まずコアスレッドでタスクを実行
現在の実行スレッド数が corePoolSize より小さい 場合、スレッドプールは直接新しいスレッドを作成してタスクを実行します。
2)コアスレッドが満杯 → タスクはキューへ
現在のスレッド数が すでに corePoolSize に達している場合、新しいタスクはすぐにはスレッド作成をトリガーせず、BlockingQueue の タスクキュー に入って実行を待ちます。
3)キューが満杯 → スレッドプールを拡張
タスクキューが最大容量に達しており、かつ現在のスレッド数が maximumPoolSize より小さい 場合、スレッドプールは 新しいスレッドを作成してタスクを処理 します。これらのスレッドは 非コアスレッド と呼ばれます。
4)最大スレッド数を超過 → 拒否ポリシー
キューが満杯で、かつ現在のスレッド数がすでに maximumPoolSize に達している場合、スレッドプールは RejectedExecutionHandler 拒否ポリシー を発動します。
主なポリシー:
- AbortPolicy(デフォルト、そのまま例外をスロー)
- CallerRunsPolicy
- DiscardPolicy
- DiscardOldestPolicy
keepAliveTime の役割
スレッドプール内の 非コアスレッド が keepAliveTime の時間内に 1 つもタスクを実行しなかった 場合、そのスレッドは破棄されます。つまりコアスレッドはデフォルトでは破棄されず、非コアスレッドはアイドル時間が keepAliveTime を超えると回収されます。これによりスレッドプールは、ピーク時に拡張し、低負荷時にスレッドを回収する ことができます。
非常にハマりやすい落とし穴
workQueue に 無界キュー(LinkedBlockingQueue のデフォルトコンストラクタなど) を使うと、タスクは延々とキューに入り続け、キューはほぼ満杯になりません。その結果、スレッドプールは永遠に拡張のステップまで到達せず、maximumPoolSize と keepAliveTime は 基本的に効きません。これは多くの本番システムで スレッドプールの設定が無効になる よくある原因でもあります。
まとめ
スレッドプールの拡張経路は、コアスレッド → タスクキュー → 非コアスレッド → 拒否ポリシー、と要約できます。maximumPoolSize はキューが満杯になって初めて効き、keepAliveTime は非コアスレッドの回収タイミングを決めます。スレッドプールを設定する際は、キューの種類の選択がスレッド数のパラメータと同じくらい重要です。無界キューは拡張と拒否ポリシーを有名無実化してしまいます。この一連のチェーン全体を理解すれば、本番で起きる「スレッドプールのパラメータが効かない」という問題の大半に答えが見つかります。
COMMENTS