【プログラミングしてみよう53】JavaのExecutorServiceでスレッドプールを使う

Java

前回はWeakReferenceとSoftReferenceを使ったメモリ管理を学びました。

今回は「ExecutorService(エグゼキュータサービス)」について解説します。 これまでのマルチスレッドでは new Thread() で直接スレッドを作っていましたが、ExecutorService を使うと「スレッドプール」でスレッドを効率よく再利用でき、より実践的な並行処理が書けるようになります。

ExecutorServiceとスレッドプールとは

new Thread() でスレッドを毎回作ると、スレッドの生成・破棄のコストが高く、大量のタスクでは処理が重くなります。

「スレッドプール」はあらかじめ決まった数のスレッドを用意しておき、タスクが来たら空いているスレッドに割り当て、終わったらそのスレッドを再利用する仕組みです。

// 毎回スレッドを作る(非効率)
new Thread(() -> task()).start();
new Thread(() -> task()).start();
new Thread(() -> task()).start();

// スレッドプールで再利用(効率的)
ExecutorService pool = Executors.newFixedThreadPool(3);
pool.execute(() -> task());
pool.execute(() -> task());
pool.execute(() -> task());

Executors クラスにはよく使うスレッドプールを作るファクトリメソッドが用意されています。

Executors.newFixedThreadPool(n)    // n個のスレッドで固定
Executors.newCachedThreadPool()    // 必要に応じてスレッドを増減
Executors.newSingleThreadExecutor() // 1つのスレッドで順番に実行
Executors.newScheduledThreadPool(n) // スケジュール実行に対応

サンプルコード(基本的なExecutorServiceの使い方)

ファイル作成

「Java41」フォルダに ExecutorTest.java という名前でファイルを作成します

コード入力

以下のコードを入力します。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ExecutorTest {
    public static void main(String[] args) throws InterruptedException {
        // 3スレッドのプールを作成
        ExecutorService pool = Executors.newFixedThreadPool(3);

        System.out.println("タスクを送信します");

        // 6つのタスクを送信(3スレッドで並行処理)
        for (int i = 1; i <= 6; i++) {
            final int taskId = i;
            pool.execute(() -> {
                String threadName = Thread.currentThread().getName();
                System.out.println("タスク" + taskId + " 開始 [" + threadName + "]");
                try {
                    Thread.sleep(500); // 0.5秒かかる処理
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                System.out.println("タスク" + taskId + " 完了 [" + threadName + "]");
            });
        }

        System.out.println("すべてのタスクを送信しました");

        // 新しいタスクの受付を停止し、実行中のタスクが完了するのを待つ
        pool.shutdown();
        pool.awaitTermination(10, TimeUnit.SECONDS);

        System.out.println("すべてのタスクが完了しました");
    }
}

コンパイル

javac ExecutorTest.java

実行

java ExecutorTest

実行結果は次のようになります(スレッドの順番は毎回異なります)。

タスクを送信します
すべてのタスクを送信しました
タスク2 開始 [pool-1-thread-2]
タスク1 開始 [pool-1-thread-1]
タスク3 開始 [pool-1-thread-3]
タスク1 完了 [pool-1-thread-1]
タスク3 完了 [pool-1-thread-3]
タスク2 完了 [pool-1-thread-2]
タスク4 開始 [pool-1-thread-1]
タスク5 開始 [pool-1-thread-3]
タスク6 開始 [pool-1-thread-2]
タスク5 完了 [pool-1-thread-3]
タスク4 完了 [pool-1-thread-1]
タスク6 完了 [pool-1-thread-2]
すべてのタスクが完了しました

サンプルコード(Callableで結果を受け取る)

execute() はタスクの結果を返しませんが、submit() と Callable を使うと結果を Future として受け取れます。

ファイル作成

「Java41」フォルダに CallableTest.java という名前でファイルを作成します

コード入力

以下のコードを入力します。

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class CallableTest {
    // 結果を返すタスク
    static int heavyCalculation(int n) throws InterruptedException {
        Thread.sleep(300); // 重い計算のシミュレート
        return n * n;
    }

    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(4);
        List<Future<Integer>> futures = new ArrayList<>();

        long start = System.currentTimeMillis();

        // 8つの計算タスクを並行して実行
        for (int i = 1; i <= 8; i++) {
            final int num = i;
            Callable<Integer> task = () -> heavyCalculation(num);
            Future<Integer> future = pool.submit(task);
            futures.add(future);
        }

        // すべての結果を収集
        System.out.println("--- 計算結果 ---");
        for (int i = 0; i < futures.size(); i++) {
            int result = futures.get(i).get(); // 完了まで待って結果を取得
            System.out.printf("%d の二乗 = %d%n", i + 1, result);
        }

        long elapsed = System.currentTimeMillis() - start;
        System.out.println("\n経過時間: " + elapsed + "ms");
        System.out.println("(同期で実行した場合は約2400ms)");

        pool.shutdown();
    }
}

コンパイル

javac CallableTest.java

実行

java CallableTest

実行結果は次のようになります。

--- 計算結果 ---
1 の二乗 = 1
2 の二乗 = 4
3 の二乗 = 9
4 の二乗 = 16
5 の二乗 = 25
6 の二乗 = 36
7 の二乗 = 49
8 の二乗 = 64

経過時間: 630ms
(同期で実行した場合は約2400ms)

コードの解説

Executors.newFixedThreadPool(3) は3つのスレッドを持つプールを作成します。6つのタスクを送信しても、同時に実行されるのは3つまでで、1つが完了すると次のタスクがそのスレッドで実行されます。スレッド名に pool-1-thread-1 のように番号が付いており、同じスレッドが再利用されているのが確認できます。

pool.shutdown() は新しいタスクの受付を停止します。すでに送信済みのタスクは引き続き実行されます。pool.awaitTermination(10, TimeUnit.SECONDS) はすべてのタスクが完了するか10秒が経過するまで待ちます。shutdown() を呼ばないとプログラムが終了しないため、必ず呼ぶようにしましょう。

Callable<T> は Runnable と似ていますが、結果を返せる(T 型の値を返す)点と、チェック例外を投げられる点が違います。pool.submit(callable) で Future<T> が返り、future.get() でタスクが完了するまで待って結果を取り出します。

4スレッドで8つのタスク(各300ms)を実行した場合、2回に分けて実行されるため約600msで完了します。同期処理なら8 × 300ms = 2400msかかるところを、並行処理で大幅に短縮できていることが確認できます。

まとめ

ExecutorService を使ったスレッドプールにより、スレッドの再利用と効率的な並行処理が実現できます。execute() で結果なしのタスクを、submit() と Callable で結果ありのタスクを実行し、Future.get() で結果を受け取りましょう。使い終わったら必ず shutdown() を呼ぶことも重要です。new Thread() を直接使う代わりに ExecutorService を使うことで、スレッド数の管理がしやすくなり、より安全で効率的なマルチスレッドプログラムが書けるようになります。

2026/08/01(土) ぱんまる

コメント

タイトルとURLをコピーしました