Java ExecutorService のスレッドプール設定:IO密集型とCPU密集型タスクへの最適化

前提:直列実行 vs 並列実行の比較

以下のように、各メソッドが100msスリープする単純なタスクを4つ直列に実行した場合、全体の実行時間は約400msとなります。

private String executeTaskA() {
    try {
        Thread.sleep(100);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    return "resultA";
}

private String executeTaskB() {
    try {
        Thread.sleep(100);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    return "resultB";
}

private String executeTaskC() {
    try {
        Thread.sleep(100);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    return "resultC";
}

private String executeTaskD() {
    try {
        Thread.sleep(100);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    return "resultD";
}

// 直列実行(例:mainメソッド内)
long start = System.currentTimeMillis();
List<String> results = List.of(
    executeTaskA(),
    executeTaskB(),
    executeTaskC(),
    executeTaskD()
);
long duration = System.currentTimeMillis() - start;
System.out.println("直列実行時間: " + duration + "ms");
// → 約400ms になる

一方で、上記のタスクをExecutorServiceで並列に実行すると、時間が大幅に shortening されます。ここでは、利用可能なCPUコア数に基づいた適切なスレッドプールサイズの設定方法を示します。

// 利用可能な論理コア数の既定値 +1 をプールサイズに
int poolSize = Runtime.getRuntime().availableProcessors() + 1;
ExecutorService executor = Executors.newFixedThreadPool(poolSize);

// 各タスクを非同期でサブミット
Future<String> futureA = executor.submit(() -> executeTaskA());
Future<String> futureB = executor.submit(() -> executeTaskB());
Future<String> futureC = executor.submit(() -> executeTaskC());
Future<String> futureD = executor.submit(() -> executeTaskD());

List<Future<String>> futures = List.of(futureA, futureB, futureC, futureD);

// 結果の取得とエラーハンドリング
List<String> results = futures.stream()
    .map(f -> {
        try {
            return f.get();
        } catch (Exception e) {
            throw new RuntimeException("タスク実行エラー", e);
        }
    })
    .collect(Collectors.toList());

executor.shutdown();
executor.awaitTermination(60, TimeUnit.SECONDS);

// すべてのタスクが並列で約100msで終わるため、全体実行時間は~100–200ms程度
long duration = System.currentTimeMillis() - start;
System.out.println("並列実行時間: " + duration + "ms");

スレッドプールサイズの推奨値

  • IO密集型タスク: I/O待機時間が長くCPU利用率が低い場合、スリープ中に他のタスクを実行できるよう、スレッド数を多く設定すると効果的です。目安は 2 × CPUコア数 ですが、実環境に応じて調整が必要です。
  • CPU密集型タスク: 継続的にCPUを使用する処理では、コンテキストスイッチのオーバーヘッドを避けるため、スレッド数を最小限に抑えるべきです。推奨は CPUコア数 + 1 であり、余裕を持たせるため+1します。

※ 実際にはタスクの性質、JVM・OSのスケジューリング挙動、メモリやI/O帯域などの要因が影響するため、本手法を上限rather than 絶対的なルールとして捉え、プロダクション環境ではベンチマークで検証することが重要です。

タグ: Java ExecutorService concurrency thread-pool IO-bound

6月6日 18:07 投稿