🎃

マルチスレッドの基本<後編>[Java入門]

に公開

はじめに

こんにちは。
プログラミング初心者wakinozaと申します。
Java勉強中に調べたことを記事にまとめています。

十分気をつけて執筆していますが、なにぶん初心者が書いた記事なので、理解が浅い点などあるかと思います。
間違い等あれば、指摘いただけると助かります。

記事を参考にされる方は、初心者の記事であることを念頭において、お読みいただけると幸いです。

対象読者

  • Javaを勉強中の方
  • Java SE11 Gold試験を勉強中の方
  • JavaのFutureインターフェースとCallableインターフェースについて知りたい方

目次

1. スレッド結果の受け取り方
2. Futureインターフェースの主なAPI
3. Runnableインターフェースを利用する場合
4. Callableインターフェースを利用する場合
5. RunnableとCallableの見分け方

この記事では、Javaのマルチスレッド操作のうち、FutureインターフェースとCallableインターフェースについて解説していきます。

マルチスレッドの概略、Threadクラス、Runnableインターフェースについて知りたい方は、前編の記事をご覧ください。

https://zenn.dev/wakinoza/articles/a12e8b1e1d5286

Executorフレームワークについて知りたい方は、中編の記事をご覧ください。

https://zenn.dev/wakinoza/articles/ba437456e44bec

本文

1. スレッド結果の受け取り方

Javaの並列処理では、処理を別のスレッドで実行し、その結果をメインスレッドで受け取るというパターンがよくあります。このパターンを実現するのが、java.util.concurrent.Futureインターフェースです。
別スレッドの実行結果を取得できるようになると、結果に応じて別スレッドを生成したり、別の処理を継続しているスレッドを止めたりという、より細やかな処理が可能になります。

Futureは「インターフェース」ですが、開発者が任意に実装する必要はありません。
java.util.concurrentパッケージ内に、Futureインターフェースを実装したクラス(FutureTaskなど)が複数用意されているためです。

ExecutorServiceやScheduledExecutorServiceのsubmitメソッドは内部で、受け取った処理をラップし、処理結果に適した実装クラスのインスタンスを作成し、戻り値として返します。
戻り値として返されるクラスは様々ですが、全てFutureインターフェースの実装クラスであるため、戻り値の型をFuture型としておけば、問題なく受け取ることができます。
この仕組みにより、開発者はsubmitメソッドが内部でどの実装クラスを返すかを気にする必要がなく、処理結果を一様に扱うことができるのです。

//例外処理を省略した記述
ExecutorService executor1 = Executors.newSingleThreadExecutor();
Future<?> future1 = executor1.submit(() -> {
    System.out.println("future");
})

executor1.shutdown();

2. Futureインターフェースの主なAPI

Futureインターフェースの主なAPIは、以下の通りです。

メソッド 内容 throws
V get() スレッドの処理完了を待ち、処理が終われば結果を返す 待機中に割り込みが発生するとInterruptedExceptionが、タスクの実行中に例外が発生するとExecutionExceptionがスローされる
V get(long timeout, TimeUnit unit) 上のgetメソッドと同様だが、引数で指定された時間を超えても処理が終わらない場合は、TimeOutExceptionがスローされる 待機中に割り込みが発生するとInterruptedExceptionが、タスクの実行中に例外が発生するとExecutionExceptionが、タイムアウトするとTimeOutExceptionがスローされる
boolean isDone() 処理が完了している場合は、trueを返す
boolean cancel(boolean mayInterruptIfRunning) 処理の取り消しを試みます。処理を取り消しできた場合はtrueを、できなかった場合はfalseを返す。cancelメソッドの戻り値は「キャンセル要求が後続の処理に影響を与えるか」を示すもので、タスクが実際に停止したことを保証しない。そのため、最終的なキャンセル状態の確認には、次のisCancelledメソッドを利用する
boolean isCancelled() 処理が正常に完了する前に取り消しできた場合は、trueを返す

3. Runnableインターフェースを利用する場合

submitメソッドには、Runnableインターフェースを引数とする場合と、Callableインターフェースを引数とする場合があります。
この節では、Runnableインターフェースを引数とした場合のFutureについて説明します。

Runnableインターフェースは、抽象メソッドであるrunメソッドを1つだけ持つ関数型インターフェースです。

void run()

runメソッドは上の通り、何も受け取らず、何も返さないメソッドです。
そのため、getメソッドでスレッドの処理結果を取得しても、結果は常に「null」となります。
以下が例外処理を記述したコード例です。

ExecutorService executor2 = Executors.newSingleThreadExecutor();
try {
    Runnable runnableTask = () -> {
        System.out.println("Runnable");
    };
    Future<?> future2 = executor2.submit(runnableTask);

    try {
        if (future2.get() == null) {
            System.out.println("end");
        }
    } catch (InterruptedException e) {
        //外部から割り込みがかかった場合
        System.err.println("タスクの待機中に割り込みが発生しました。");
        Thread.currentThread().interrupt();
        // スレッドの割り込みステータスを再設定
    } catch (ExecutionException e) {
        // タスクの実行中に何らかの例外が発生した場合
        System.err.println("タスクの実行中に例外が発生しました。");
        e.getCause().printStackTrace();
    }
} finally {
    executor2.shutdown();
}

getメソッドはInterruptedExceptionとExecutionExceptionをスローするため、それぞれの例外処理が必要です。また、別スレッドの処理が完了後にスレッドプールのシャッドダウン処理が必要であるため、finallyブロックにshutdownメソッドを記述します。

もしnull以外の戻り値を指定したい場合は、submitメソッドの第2引数に、戻り値を指定します。

ExecutorService executor3 = Executors.newSingleThreadExecutor();
try {
    Runnable runnableTask2 = () -> {
        System.out.println("Runnable2");
    };
    Future<String> future3 = executor3.submit(runnableTask2,"finish");
    //submitメソッドの第2引数に値を指定
    try {
        String result3 = future3.get();
        System.out.println(result3);
        //結果:finish

    } catch (InterruptedException e) {
        System.err.println("タスクの待機中に割り込みが発生しました。");
        Thread.currentThread().interrupt();
    } catch (ExecutionException e) {
        System.err.println("タスクの実行中に例外が発生しました。");
        e.getCause().printStackTrace();
    }
} finally {
    executor3.shutdown();
}

上のコードでは、submitメソッドの第2引数にStringクラスの"finish"を指定しました。
戻り値を指定すると、getメソッドはスレッドの処理完了を待って、指定された戻り値"finish"を返します。
この時、Futureのジェネリクスと処理結果を受け取る変数の型が指定した戻り値の型になる点に注意してください。

4. Callableインターフェースを利用する場合

Runnable型の処理を実行する場合、スレッドの処理結果は、「null」もしくは指定した固定値しか返すことができません。
実際の処理結果を戻したり、必要に応じて例外をスローするためには、Callableインターフェースを利用します。

Callableインターフェースは、抽象メソッドであるcallメソッドを1つだけ持つ関数型インターフェースです。

V call() throws Exception

結果を処理して戻り値を返し、結果を計算できなかった場合にはExceptionをスローします。
submitの引数にCallableを指定すると、処理結果がFutureインターフェースを実装したクラスとして返されます。また、処理結果をgetメソッドで取得することも可能です。


ExecutorService executor4 = Executors.newSingleThreadExecutor();
try {
    Callable<String> callableTask = () -> {
        return "CallableTask";
    };
    Future<String> future4 = executor4.submit(callableTask);

    try {
        String result4 = future4.get();
        System.out.println(result4);
        //結果:CallableTask
    } catch (InterruptedException e) {
        System.err.println("タスクの待機中に割り込みが発生しました。");
        Thread.currentThread().interrupt();
    } catch (ExecutionException e) {
        System.err.println("タスクの実行中に例外が発生しました。");
        e.getCause().printStackTrace();
    }
} finally {
    executor4.shutdown();
}

Callableにおいても、Futureのジェネリクスと処理結果を受け取る変数の型が、処理結果の型になる点に注意してください。

また、Callableを利用すると、任意の例外をスローすることもできます。

final int number = 1;

ExecutorService executor5 = Executors.newSingleThreadExecutor();
try {
    Callable<String> callableTask2 = () -> {
        if (number % 2 == 1) {
            throw new Exception();
        }
        return "CallableTask2";
    };
    Future<String> future5 = executor5.submit(callableTask2);

    try {
        String result5 = future5.get();
        System.out.println(result5);
    } catch (InterruptedException e) {
        System.err.println("タスクの待機中に割り込みが発生しました。");
        Thread.currentThread().interrupt();
    } catch (ExecutionException e) {
        System.err.println("タスクの実行中に例外が発生しました。");
        e.getCause().printStackTrace();
        //別スレッド内で発生した例外はExecutionExceptionの内部に保持されている
        // getCauseメソッドで保持された例外を呼び出し、スタックトレースを表示
    }
} finally {
    executor5.shutdown();
}

別スレッドで例外が発生しても、すぐに呼び出し元のスレッドにスローされるわけではありません。スローされた例外は、結果であるFutureが一旦受け取ります。
その後、Futureオブジェクトのgetメソッドを呼び出すと、java.util.concurrent.ExecutionExceptionをスローします。スレッド内で発生した例外はExecutionExceptionの内部に保持されているため、getCauseメソッドで取り出します。

5. RunnableとCallableの見分け方

RunnableとCallableはともに関数型インターフェースであるため、submitメソッドの引数に直接ラムダ式での記述して宣言することも可能です。コードが簡潔になりますが、困ったことも起きます。
submitメソッドに直接ラムダ式を記述すると、そのコードがRunnableなのかCallableなの判別しにくくなるのです。

//Runnable
ExecutorService executor6 = Executors.newSingleThreadExecutor();
executor6.submit(() -> {
        System.out.println("Task");
    });

//Callable
ExecutorService executor7 = Executors.newSingleThreadExecutor();
executor7.submit(() -> {
        System.out.println("Task2");
        return "Task3";
    });

このような場合は、戻り値があるかどうかを確認しましょう。
Runnableには戻り値はありませんが、Callableには戻り値があります。戻り値の有無で見分けると良いでしょう。

まとめ

  • JavaのFutureインターフェースは、別スレッドで実行した処理結果を受け取るための仕組みです。ExecutorServiceのsubmitメソッドにRunnableやCallableを渡すことでFutureオブジェクトを取得できます

  • 処理結果の戻り値や、検査例外をスローしたい場合はRunnableの代わりにCallableインターフェースを利用します

  • Callable内でスローされた例外は、get()メソッド呼び出し時にExecutionExceptionにラップされて呼び出し元に通知されます

  • インターフェース名の記述がない場合は、戻り値がない方はRunnable、戻り値がある方はCallableと見分けましょう


記事は以上です。
最後までお読みいただき、ありがとうございました。

参考情報一覧

この記事は以下の情報を参考にして執筆しました。

GitHubで編集を提案

Discussion