Java | Java 標準ライブラリ:Future

Java Java
スポンサーリンク

Future を一言でいうと

java.util.concurrent.Future は、

「別スレッドで走っている“将来の計算結果”を受け取るための“約束の箱”」

です。

時間のかかる処理を ExecutorService に投げると、
その場で結果は返ってきません。代わりに Future が返されます。

その Future に対して、

  • 終わったかどうか確認する
  • 終わるまで待って結果をもらう
  • タイムアウトを決めて待つ
  • もういらないからキャンセルする

といった操作ができます。

「非同期処理のハンドル」としての役割を持っているのが Future です。


まずは全体像:Future が関わる典型パターン

ExecutorService との関係

Future は、単体で new することは基本的にありません。
普通は ExecutorServicesubmit メソッドを使うときに、一緒に出てきます。

ExecutorService executor = Executors.newSingleThreadExecutor();

Future<Integer> future = executor.submit(() -> {
    // 時間のかかる処理
    Thread.sleep(2000);
    return 42;
});
Java

ここで起きていることは、

  • submit した瞬間、別スレッドで処理が開始される
  • その結果(ここでは Integer)を、後で受け取るための Future<Integer> が返ってくる

ということです。

すぐに結果は返ってこない。
でも「そのうち終わるはず」という“約束”だけは手元にある。
その約束を表現しているのが Future です。


一番大事なメソッド:get() で結果を受け取る

get() は「終わるまで待つ」メソッド

Future の本命メソッドは get() です。

Integer result = future.get();
System.out.println("結果 = " + result);
Java

get() は、

  • タスクがまだ終わっていなければ、「終わるまでブロック(待機)」する
  • 終わっていれば、即座に結果を返す

という挙動をします。

つまり、

「とりあえず非同期に投げておいて、必要になったタイミングで結果を取りに行く」

という書き方ができます。

get() が投げる 2 種類の例外

get() はチェック例外を 2 つ投げる可能性があります。

InterruptedException
待っている間に現在のスレッドが割り込まれたとき

ExecutionException
タスクの中で例外が発生したとき

例を見てみましょう。

import java.util.concurrent.*;

public class FutureGetExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();

        Future<Integer> future = executor.submit(() -> {
            System.out.println("タスク開始");
            Thread.sleep(2000);
            if (true) {
                throw new IllegalStateException("何かがおかしい");
            }
            // return 42;
        });

        try {
            Integer result = future.get();  // ここで例外が飛ぶ
            System.out.println("結果 = " + result);
        } catch (InterruptedException e) {
            System.out.println("待っている間に割り込まれました");
        } catch (ExecutionException e) {
            System.out.println("タスク内で例外: " + e.getCause());
        }

        executor.shutdown();
    }
}
Java

タスク内で IllegalStateException を投げています。
その結果、future.get() では ExecutionException が投げられ、

e.getCause()
で「元の例外(ここでは IllegalStateException)」にたどり着けます。

ここが重要です。

タスク内の例外は、get() を呼んだときに初めて表面化する。
そのときは ExecutionException に包まれている。

というルールを、しっかり頭に入れておいてください。


タイムアウト付きの get:待ちすぎないための仕組み

get(long timeout, TimeUnit unit)

ときには「結果をいつまでも待つわけにはいかない」こともあります。

例えば、3 秒以内に結果が来なければ諦めたい、という場合。

import java.util.concurrent.*;

public class FutureTimeoutExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();

        Future<Integer> future = executor.submit(() -> {
            Thread.sleep(5000); // 5秒かかる
            return 123;
        });

        try {
            System.out.println("3秒だけ待つ…");
            Integer result = future.get(3, TimeUnit.SECONDS);
            System.out.println("結果 = " + result);
        } catch (TimeoutException e) {
            System.out.println("タイムアウトしました。もう待ちません。");
            future.cancel(true); // 後で説明
        } catch (InterruptedException e) {
            System.out.println("待っている間に割り込まれました");
        } catch (ExecutionException e) {
            System.out.println("タスク内で例外: " + e.getCause());
        }

        executor.shutdown();
    }
}
Java

get(3, TimeUnit.SECONDS) は、

  • タスクが 3 秒以内に終われば結果を返す
  • 終わらなければ TimeoutException を投げる

という挙動です。

タイムアウト時にどうするかは設計次第ですが、
「キャンセルを試みる」のが一般的なパターンのひとつです(後述)。


isDone / isCancelled / cancel:タスクの状態を確認・変更する

isDone() で「終わったかどうか」だけを知る

get() は「終わるまで待つ」メソッドですが、
ただ「終わったかどうか」だけを知りたいときは isDone() を使います。

Future<Integer> future = executor.submit(() -> {
    Thread.sleep(2000);
    return 42;
});

while (!future.isDone()) {
    System.out.println("まだ終わってない…");
    Thread.sleep(500);
}

System.out.println("完了!結果 = " + future.get());
Java

この例では、半秒おきに状態をポーリングして、
isDone() が true になったら get() しています。

実務では、無限ループでポーリングするのはあまり綺麗ではありませんが、

「get() はブロックする」
「isDone() はブロックせずに状態だけ見れる」

という違いを押さえておくことが重要です。

cancel(boolean mayInterruptIfRunning) でキャンセルを試みる

Future#cancel(boolean) は、タスクのキャンセルを試みます。

boolean cancelled = future.cancel(true);
System.out.println("キャンセル成功?: " + cancelled);
Java

引数の mayInterruptIfRunning には、

  • まだ開始されていないタスク → 常にキャンセルされる(実行キューから外される)
  • すでに実行中のタスク → true ならスレッドに割り込みをかける/false なら何もしない

という意味があります。

ただし、「キャンセルを試みる」だけであって、
実際にタスクがすぐ止まるかどうかは、そのタスクの書き方次第です。

タスク側は、

  • Thread.sleep など割り込みに反応するメソッドを正しく使っている
  • 自分で Thread.currentThread().isInterrupted() をチェックして早期リターンする

など、「割り込みに優しく書かれている」必要があります。

キャンセル後は、

future.isCancelled() → true
future.isDone() → true

になります。

get() を呼ぶと CancellationException が投げられます。

isCancelled() でキャンセルされたかどうかを見る

キャンセルされたかどうかだけ知りたいときは isCancelled() を使います。

if (future.isCancelled()) {
    System.out.println("このタスクはキャンセルされました");
}
Java

isDone() は「正常終了か例外かキャンセルかに関わらず、とにかく終わった」
isCancelled() は「キャンセルで終わった」

という違いだと理解してください。


Runnable を submit したときの Future(戻り値なしの場合)

Future<?> の get() は何を返すのか

ExecutorService#submit(Runnable task)Future<?> を返します。

Future<?> future = executor.submit(() -> {
    System.out.println("何かするけど、戻り値はいらないタスク");
});
Java

この場合、get() は何を返すでしょうか?

答えは「null です」。

Object result = future.get(); // null
Java

戻り値は特に意味がないので、
「完了(または失敗/キャンセル)を待つため」に使うのが普通です。

たとえば、

Future<?> future = executor.submit(longRunningRunnable);

// 別のことをして…

// 最後に「終わるのを待って」から終了したい
future.get();
Java

のように、「task の完了を待つ(例外があれば ExecutionException として検知する)」
という用途で使います。


例題でまとめて流れを掴む

例:複数の Web API 呼び出しを並列に行って、一番遅いものまで待つ

疑似的な「時間のかかる API 呼び出し」を 3 つ並列に実行する例です。

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

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

        List<Callable<String>> tasks = List.of(
            () -> {
                Thread.sleep(1000);
                return "API-1 の結果";
            },
            () -> {
                Thread.sleep(2000);
                return "API-2 の結果";
            },
            () -> {
                Thread.sleep(1500);
                return "API-3 の結果";
            }
        );

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

        for (Callable<String> task : tasks) {
            futures.add(executor.submit(task));
        }

        System.out.println("すべての API を並列に呼び出しました。結果を待ちます…");

        for (Future<String> f : futures) {
            String result = f.get(); // 1つずつ完了を待つ
            System.out.println("結果: " + result);
        }

        executor.shutdown();
    }
}
Java

このコードでは、

  • 3 つの API 呼び出しタスクをほぼ同時に開始している
  • それぞれは別スレッドで走る
  • f.get() を順番に呼ぶことで、「全部終わるまで」待っている
  • 合計時間は最も遅いタスク(ここでは 2000ms)に近くなる

という挙動になります。

Future を使うことで、

「並列で投げておいて、後から順番に結果を回収する」

というパターンが、素直に表現できます。


注意点:Future で“非同期を扱っているつもり”にならない

get() をすぐ呼ぶと「ただの同期処理」に逆戻りする

よくある悪いパターンとして、

Future<Result> f = executor.submit(task);
Result r = f.get(); // すぐ呼ぶ
Java

という書き方があります。

これだと、

タスクを別スレッドに投げた直後に、
メインスレッドで「終わるまで待ち続ける」ので、

「new Thread して join する」のと本質的にあまり変わりません。

Future の良さは、

  • 「今すぐ結果がいらない」場面では、投げっぱなしにできる
  • 後で必要になったときにだけ、get() で待つ
  • または isDone() で状態を見ながら、他のことを並行して進める

という柔軟さにあります。

常に submit 直後に get() してしまうと、
Future の旨味を活かせていない状態になります。

「どこまで非同期で、どこから同期で受け取るか」を設計として決める

Future を使うときは、

  • タスクを投げる場所
  • 結果を受け取る場所

を意識的に分けて設計すると、コードが整理されます。

例えば、

  • コントローラ層で全部のタスクを投げて、
  • サービス層で get() してまとめる

とか、

  • サービス層でタスクを投げて、
  • さらに別の集約メソッドで get() して合成する

など、「非同期の範囲」をちゃんと決めることが大事です。


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

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

「ExecutorService に投げた “将来の結果” を表す約束の箱であり、
その箱に対して『待つ/タイムアウトする/キャンセルする/状態を見る』を行うためのインターフェース」

です。

特に意識してほしいのは、

  • submit の戻り値として Future を受け取り、後から get() で結果を受け取る
  • get() は完了までブロックし、タスク内例外は ExecutionException 経由で受け取る
  • get(timeout, unit) でタイムアウト付きの待機ができる
  • isDone / isCancelled / cancel で状態やキャンセルを制御できる
  • 「投げる場所」と「受け取る場所」を意識的に分けて設計すると、非同期処理が整理される

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