最終更新:

スレッドプールの仕組み — 2人の作業係に4件の仕事を渡す


仕事と作業係を、分けて考える

2人が担当、2件は待つキュー:順番を待つ仕事C 仕事Dワーカー1仕事Aワーカー2仕事B空いた係が、次を引き受ける終わった係へ、次の仕事✓ A・Bの処理が完了ワーカー1仕事Cワーカー2仕事D作業係を作り直さず、使い回す
担当の移り変わりを表す一例です。実際の開始・完了順は固定ではなく、キューや受付の上限も別に確認します。
ひよこ ひよこ
4枚の画像を作るたびに、別のスレッドを作るの?
ペンギン先生 ペンギン先生
その方法もあるけど、スレッドプールなら作業係を使い回せるよ。下の例では2つのワーカーに4件の小さな仕事を渡す。終わった係が次を引き受けるので、スレッドの準備・終了を毎回繰り返す負担を減らせるんだ。
ひよこ ひよこ
係が全員忙しいと、残りの仕事はどこへ行く?
ペンギン先生 ペンギン先生
キューで待たせる設計があるよ。ただしキューにも容量や扱い方がある。待ち行列を無制限にすると、ワーカー数を制限していても仕事がたまり、メモリや待ち時間が増え得るんだ。
ひよこ ひよこ
JavaのnewFixedThreadPoolなら安心?
ペンギン先生 ペンギン先生
固定数のスレッドを再利用するけれど、共有キューは無制限だよ。今回の4件には便利でも、本番では受付量、キューの容量、待ち時間、拒否時の扱いを別に考えよう。固定数だから全体の資源も必ず固定、ではないんだ。
ひよこ ひよこ
スレッドを増やせば、どんどん速くなる?
ペンギン先生 ペンギン先生
CPU、メモリ、接続先にも限りがあるよ。計算の仕事と通信待ちの仕事では条件も違う。最適な数を一律の公式で決めず、同時実行数、待ち時間、エラーや資源の使用量を見ながら決めるんだ。
ひよこ ひよこ
枯渇って、全員が戻ってこないこと?
ペンギン先生 ペンギン先生
全ワーカーが長い待ち処理などに使われて、新しい仕事へ回らない状態だね。同じ小さなプールの仕事が、そのプールでまだ実行されていない別の仕事を待つと、進めなくなることもある。上限だけでなく待ち方も確認しよう。
ひよこ ひよこ
ForkJoinPoolは、仕事を盗むって聞いたよ。
ペンギン先生 ペンギン先生
ワークスティーリングだね。ほかのワーカーの仕事も引き取って進める設計で、分割できる計算などに使う。単純な共有キューだけのプールとは違うし、どんなブロッキング処理でも自動で解決するわけではないよ。
ひよこ ひよこ
Javaの仮想スレッドも、使い回すの?
ペンギン先生 ペンギン先生
通常は仕事ごとに仮想スレッドを作り、仮想スレッド自体をプールしない方針だよ。待ちの多い処理をたくさん扱うための仕組みで、CPU計算そのものを速くするものではない。DB接続など希少な資源は、別に同時利用数を制御するんだ。
ひよこ ひよこ
まず何を見たら、プールを理解できる?
ペンギン先生 ペンギン先生
下の例で、4件の仕事を動かすスレッド名を見よう。開始や完了の順番は固定ではないよ。終わった仕事の結果を受け取り、最後にプールを閉じるところまで試すと、仕事と作業係を分けて考えやすいね。

まずは、2つのワーカーで4件を処理する

スレッドプールは、仕事を渡す側と、実行するスレッドの管理を分ける仕組みです。画像作成のような仕事を、決めた人数の作業係に渡すイメージで見てみましょう。

JDK 25などが使えるなら、新しいフォルダーにPoolDemo.javaをUTF-8で保存します。ここでは実際の画像処理ではなく、仕事名を表示する小さな例です。

import java.util.ArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class PoolDemo {
    public static void main(String[] args) throws Exception {
        try (var pool = Executors.newFixedThreadPool(2)) {
            var results = new ArrayList<Future<String>>();
            for (int i = 1; i <= 4; i++) {
                final int job = i;
                results.add(pool.submit(() ->
                    "job " + job + " / "
                    + Thread.currentThread().getName()
                ));
            }
            for (var result : results) {
                System.out.println(result.get());
            }
        }
    }
}
javac -encoding UTF-8 PoolDemo.java
java PoolDemo

job 1〜4と、pool-1-thread-1などのワーカー名が出ます。同じ名前が複数の仕事に現れれば、再利用が見えます。どのワーカーが何件担当するかは固定ではありません。 小さな仕事なので、2つが常に同じ量を担当する保証もありません。

get()で結果を読む順序は提出順ですが、内部で実行する順番や完了順とは別です。tryを抜けるとExecutorServiceを閉じ、仕事の終了を待ちます。

係の数だけでなく、待っている仕事も見る

newFixedThreadPoolのキューは無制限です。この例は4件だけですが、無制限に送信し続ければ、安全な上限になるわけではありません。

本番では、次をセットで考えます。

  • 同時に処理できる仕事の数。
  • 待たせる仕事の容量と、受付を止める条件。
  • 通信のタイムアウトと、仕事全体の期限。
  • 失敗・拒否したときの応答、取り消しと終了時の扱い。

JavaのThreadPoolExecutorにはcorePoolSize、maximumPoolSize、キュー、keepAliveTimeなどがあります。コアスレッドの事前起動やタイムアウトにも設定があり、「最低人数が常に作成済み」とは限りません。満杯になったときは例外、呼び出し側での実行など、拒否方針も選びます。

もう少し詳しく:待ち方と仮想スレッド

同じプール内で、実行枠を占めた仕事が、まだキューにいる別の仕事を待つ設計は避けます。処理の分離や非同期化を検討するときも、接続先の容量・ロック・依存関係を合わせて見ます。

Java 21で正式化された仮想スレッドは、仕事ごとに作る方針です。仮想スレッドをプールして数を絞る代わりに、DBなどの資源へのアクセスをセマフォ等で制限します。メモリや接続数の限界がなくなるわけではありません。

Java 24ではsynchronizedに起因するピン留めが改善されましたが、ネイティブコード等の条件は残ります。言語・ランタイムによるスケジューリングと、アプリが作る仕事のプールは分けて考えましょう。

覚え方と次の一歩

「スレッドプール」って出てきたら「仕事を渡し、作業係を使い回す仕組み」と思えばだいたいOK!

OSのプロセス管理やサーキットブレーカーと、実行枠や待ち続ける問題の関係を比べられます。

参考資料