前提:直列実行 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 絶対的なルールとして捉え、プロダクション環境ではベンチマークで検証することが重要です。