Java | Java 標準ライブラリ:ExecutorService 概要

Java Java
スポンサーリンク

ExecutorService を一言でいうと

ExecutorService は、

「スレッドを自分で new せずに、”仕事(タスク)だけ” を投げれば、いい感じに別スレッドで実行してくれる仕組み」

です。

new Thread(...).start() をあちこちで書く代わりに、

  • 実行したい処理だけを RunnableCallable に書いて
  • それを ExecutorService に渡す

ことで、スレッドの管理(生成・再利用・終了など)を一括で任せられます。

「スレッドの集合(スレッドプール)」に対して、「タスク」を投げるイメージを持ってください。


なぜ ExecutorService が必要なのか(生 Thread の問題点)

new Thread() を乱発するとどうなるか

初心者のうちは、並行処理を書こうとしてこうしがちです。

new Thread(() -> {
    // 重い処理
}).start();
Java

これ自体は間違いではありませんが、
大量にこういうコードがあると、次の問題が出てきます。

  • どのスレッドがいつ終わるのか把握しづらい
  • スレッド数が増えすぎて、CPU を無駄に消費したり、コンテキストスイッチだらけになる
  • 例外処理や終了処理をバラバラに書くことになり、管理が大変

つまり、「スレッドという資源を雑に使いがち」になります。

ExecutorService の発想:スレッドは“プール”、あなたは“仕事”だけ渡す

ExecutorService を使うと、
スレッドの生成・再利用・終了は「ひとつの場所」で管理できるようになります。

あなたがやるのは、

  • 「やってほしい仕事(タスク)」を Runnable / Callable として書く
  • そのタスクを executor.submit(...)executor.execute(...) で投げる

だけです。

スレッドを何本用意するか、
タスクをどのスレッドに割り当てるか、
終わったスレッドを再利用するか、

といった「インフラ側の話」は、ExecutorService 側が引き受けてくれます。


一番基本:固定スレッドプールを作ってタスクを実行する

Executors.newFixedThreadPool でスレッドプールを作る

まずは、固定数のスレッドを持つ ExecutorService を作る例から。

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

public class FixedThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3); // スレッド3本のプール

        for (int i = 1; i <= 10; i++) {
            int taskId = i;
            executor.submit(() -> {
                System.out.println("タスク " + taskId + " を実行中: " +
                        Thread.currentThread().getName());
                try {
                    Thread.sleep(1000); // 1秒かかる仕事だとする
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                System.out.println("タスク " + taskId + " 完了: " +
                        Thread.currentThread().getName());
            });
        }

        executor.shutdown(); // もう新しいタスクは受け付けない、完了したら終了
    }
}
Java

ここで起きていることを、言葉で整理します。

  • newFixedThreadPool(3) で、「3 本だけスレッドを使うプール」を作る
  • 10 個のタスクを submit で投げる
  • 同時に走るのは最大 3 個まで(他はキューで順番待ち)
  • タスクが終わったスレッドは、次のタスクに再利用される
  • 最後に shutdown() して、全部終わったらスレッドが終了する

new Thread(...).start() だと 10 本スレッドが立ってしまいますが、
この例では「3 本で回す」ことができます。

CPU コア数や負荷に応じて、
「何本のスレッドでタスクを回すか」を制御できることが、ExecutorService の大きな価値です。


submit と Future:結果を受け取る・例外を知る

Runnable と Callable の違い

executor.submit(...) に渡せるのは Runnable だけではありません。
Callable<V> という「戻り値を返せるタスク」も渡せます。

Runnable
run() メソッド、戻り値なし、例外はチェック例外を投げられない

Callable<V>
V call() throws Exception メソッド、戻り値あり、例外を投げられる

submit はタスクの完了を表す Future<V> を返してくれます。

例:数値計算タスクの結果を Future で受け取る

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class CallableExample {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        List<Future<Integer>> futures = new ArrayList<>();

        for (int i = 1; i <= 5; i++) {
            int n = i;
            Future<Integer> future = executor.submit(() -> {
                System.out.println("計算タスク " + n + " 実行中");
                Thread.sleep(1000);
                return n * n; // n の2乗を返す
            });
            futures.add(future);
        }

        for (Future<Integer> f : futures) {
            Integer result = f.get(); // 完了を待って結果を受け取る
            System.out.println("結果: " + result);
        }

        executor.shutdown();
    }
}
Java

ここで重要なポイントは、

  • submit はすぐ戻ってくる(非同期にタスクが開始される)
  • Future#get() を呼ぶと、そのタスクが終わるまで待ち、結果を返してくれる
  • タスク内で例外が起きた場合は、get() 時に ExecutionException として検知できる

ということです。

new Thread(...).start() だと、「そのスレッドでの結果や例外を、呼び出し側から扱いにくい」ですが、
ExecutorService + Future だと、「タスクの完了・結果・失敗」をオブジェクトとして扱えるようになります。


shutdown / shutdownNow とライフサイクル管理

ExecutorService を作ったら、必ず「どこで止めるか」を決める

ExecutorService は内部でスレッドを持っているので、
使い終わったら止めてあげないとスレッドリークにつながります。

基本の流れはこうです。

ExecutorService executor = Executors.newFixedThreadPool(3);
// タスクをいろいろ submit する

executor.shutdown(); // 新規タスク受付停止、実行中・待ち行列分が終わるまで待つ
Java

shutdown() は「柔らかい停止」です。
すでにキューに入っているタスクはそのまま最後まで実行されます。

完全に終わったかどうか確認したいときは、awaitTermination を使うこともあります。

executor.shutdown();
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
    executor.shutdownNow(); // まだ終わらないなら、強制停止を試みる
}
Java

shutdownNow は「強制寄り」の停止

shutdownNow() は、実行待ちのタスクをキャンセルし、
実行中のスレッドに割り込み(interrupt)をかけます。

List<Runnable> notStartedTasks = executor.shutdownNow();
Java

notStartedTasks には、まだ始まっていなかったタスクが返ってきます。

強制停止なので、
「タスクが割り込みにきちんと反応するように書かれているか」
が重要になります。

初心者のうちは、

  • 基本は shutdown() で穏やかに終了
  • 本当に止まらない時だけ shutdownNow()

くらいの意識で十分です。


ExecutorService のよくある種類(ざっくりイメージ)

newFixedThreadPool:固定本数で回す

すでに出てきた newFixedThreadPool(n) は、

「常に最大 n 本のスレッドでタスクを処理する」プールです。

CPU コア数や負荷を見て、「このくらいで回したい」という時に使います。

newCachedThreadPool:必要に応じてスレッド増やして、アイドルは破棄

newCachedThreadPool() は、
「必要に応じてスレッドを増やし、使われないスレッドは破棄する」という、伸び縮みするプールです。

短命なタスクを大量にさばきたいが、数は一定ではない、
といったケースに向きます。

ただし、「思った以上にスレッドが増えすぎる」危険もあるので、
初心者のうちは固定プールの方が挙動を理解しやすいです。

newSingleThreadExecutor:1 本だけの順次実行

newSingleThreadExecutor() は、
1 本だけスレッドを持ち、タスクを一つずつ順番に処理する ExecutorService です。

シングルスレッドなのに Executor を使う意味は、

  • タスクのキューイングや終了処理を Executor に任せられる
  • ExecutorService としての共通 API(submit, shutdown)が使える

といったところにあります。

GUI アプリの「イベントディスパッチスレッド」のようなイメージで、
「常に一つずつ順番に処理したい」場面で使えます。


ExecutorService を使うときに意識したいこと

「スレッドを意識するコード」と「仕事の中身」を分離できる

ExecutorService を使うと、

スレッド数を何本にするか
タスクをどのスレッドで実行するか

といった「並行実行の戦略」は Executor に任せ、

「タスクの中で何をするか」は Runnable / Callable に閉じ込める

という分離ができます。

これにより、

  • 並行処理の設計と、ビジネスロジックの設計を混ぜずに済む
  • テスト時には単一スレッドの Executor に差し替える、といったこともやりやすくなる

といったメリットが生まれます。

例外処理・キャンセル・タイムアウトまで含めて「タスク」の単位で考える

ExecutorService を使うと、「タスク」が一つの単位になります。

  • そのタスクが失敗したとき、どこでログを取るか
  • どこまで結果を待って、どこからタイムアウト扱いにするか
  • 途中でキャンセルできるようにするか(Future#cancel

などを、「タスクごと」に設計できます。

例えば、ある計算は 3 秒まで待つが、それ以上かかったら諦めたい、といったとき、

Future<Result> f = executor.submit(callableTask);
try {
    Result r = f.get(3, TimeUnit.SECONDS);
} catch (TimeoutException e) {
    f.cancel(true); // 割り込みをかけてキャンセル試行
}
Java

のように、「待つ側の都合」に合わせた書き方ができます。


まとめ:ExecutorService 概要を自分の中でこう位置づける

ExecutorService を初心者向けにまとめると、

「スレッドを自前で管理せず、“タスク” という単位で並行処理を投げられる仕組み(スレッドプールの顔をした実行サービス)」

です。

押さえておきたいのは、

  • Executors.newFixedThreadPool などでプールを作る
  • 実行したい処理は Runnable / Callable に書き、submit で投げる
  • 戻り値や例外は Future 経由で扱える
  • 使い終わったら shutdown()(必要なら shutdownNow())で止める
  • 「スレッドをどう使うか」と「何をするか」をきれいに分離できる

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