🧵

別スレッドで実行した処理の結果を待ちたい

に公開

環境

JDK 25

Java 7以降であれば内容は変わらないと思います。たぶん。

先に読んでほしい記事

Javaのスレッドについては👇️を読んでください。

https://zenn.dev/masatoshi_tada/articles/d6e57c8a0247c7

別スレッドでの計算結果を使いたい!

ということはよくあると思います。例えば、

  • 何らかのWeb APIへのリクエストを別スレッドで行ってそのレスポンスで帰ってきたデータを呼び出し元スレッドで使いたい
  • 複数スレッドで分担した処理の結果を、最後にマージしたい

とか。

Future を使えば、そんな処理を簡単に行うことができます。

https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/Future.html

Javadocの説明文の冒頭には、こんなことが書いてあります。

A Future represents the result of an asynchronous computation. Methods are provided to check if the computation is complete, to wait for its completion, and to retrieve the result of the computation.

Futureは非同期処理の結果を表します。メソッドは、処理が完了したかどうかを確認したり、完了を待ったり、処理結果を受け取ったりするために用意されています。」って感じですね。

ExecutorServiceとの関係

ExecutorServiceで、引数の処理を非同期で実行するsubmit()メソッドの戻り値がFutureになっています。

https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/ExecutorService.html#submit(java.util.concurrent.Callable)

Callableインタフェースは、call()というメソッドのみを持つ関数型インタフェースです。Runnableインタフェースのrun()メソッドとの違いとしては、

  1. call()は戻り値を返せる
  2. call()はチェック例外Exceptionをスローできる

の2点です。

Futureの使い方

では実際の使い方を見てみましょう。

まずはCallable実装クラスを作成します。ここに記述した処理が非同期で実行されます。

今回は、ログ出力とスリープ後に固定値を返すという単純な実装にしておきます。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

public class MyCallable implements Callable<String> {
    private static final Logger logger = LoggerFactory.getLogger(MyCallable.class);

    @Override
    public String call() throws Exception {
        logger.info("スリープ開始");
        // 2秒スリープ
        TimeUnit.SECONDS.sleep(2);
        logger.info("スリープ終了");
        return "Success";
    }
}

そして、ExecutorServiceを使ってMyCallableを非同期に実行してみます。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

public class FutureMain {
    private static final Logger logger = LoggerFactory.getLogger(FutureMain.class);

    public static void main(String[] args) {
        try (ExecutorService executorService = Executors.newFixedThreadPool(1)) {
            // 非同期で実行
            Future<String> future = executorService.submit(new MyCallable());
            logger.info("submit()しました。");
            // 非同期処理の結果を取得
            String result = future.get();
            logger.info("result = {}", result);
        } catch (Exception e) {
            logger.error("例外が発生しました。", e);
        }
    }
}

executorService.submit()で非同期処理を実行して、戻り値がFuture<String>となっています。

非同期処理の結果(今回だと"Success"という文字列)は、Futureget()メソッドで取得します。この際、非同期処理が完了するまでmainスレッドは待たされます(「ブロックされる」とも言います)。

実行結果は次の通りです。

20:37:37.551 [pool-1-thread-1] INFO future.MyCallable -- スリープ開始
20:37:37.551 [main] INFO future.FutureMain -- submit()しました。
20:37:39.554 [pool-1-thread-1] INFO future.MyCallable -- スリープ終了
20:37:39.555 [main] INFO future.FutureMain -- result = Success

ログを見ると、mainスレッドが2秒ほど待たされていることに気づきますね。これはFutureget()メソッドが、pool-1-thread-1スレッドの完了を待っているからです。

submit()メソッドのバリエーション

ExecutorServicesubmit()メソッドは3つあります。

  1. submit(Callable)
  2. submit(Runnable) : 戻り値のFutureに対してget()を呼ぶとnullが返ります。
  3. submit(Runnable, T) : 戻り値のFutureに対してget()を呼ぶと第2引数Tの値が返ります。

get()メソッドのバリエーション

Futureget()メソッドには、タイムアウトを指定できるものもあります。

https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/Future.html#get(long,java.util.concurrent.TimeUnit)

        try (ExecutorService executorService = Executors.newFixedThreadPool(1)) {
            Future<String> future = executorService.submit(new MyCallable());
            logger.info("submit()しました。");
            // 2秒かかる処理を実行しているのに、1秒でタイムアウトになる
            String result = future.get(1, TimeUnit.SECONDS);
            logger.info("result = {}", result);
        } catch (Exception e) {
            logger.error("例外が発生しました。", e);
        }

実行すると java.util.concurrent.TimeoutException がスローされます。

21:16:51.317 [main] INFO future.FutureMain -- submit()しました。
21:16:51.317 [pool-1-thread-1] INFO future.MyCallable -- スリープ開始
21:16:53.323 [pool-1-thread-1] INFO future.MyCallable -- スリープ終了
21:16:53.323 [main] ERROR future.FutureMain -- 例外が発生しました。
java.util.concurrent.TimeoutException: null
	at java.base/java.util.concurrent.FutureTask.get(FutureTask.java:206)
	at future.FutureMain.main(FutureMain.java:19)

ただし、1秒過ぎたら即座に例外がスローされる訳ではなく、2秒かかる処理が終わった直後に例外がスローされます。

Discussion