java.util.concurrent.Futureのガイド

java.util.concurrent.Futureへのガイド

1. 概要

この記事では、Futureについて学習します。 Java 1.5以降に存在し、非同期呼び出しや並行処理を処理するときに非常に役立つインターフェース。

2. Futuresの作成

簡単に言えば、Futureクラスは、非同期計算の将来の結果を表します。この結果は、処理の完了後に最終的にFutureに表示されます。

Futureインスタンスを作成して返すメソッドを作成する方法を見てみましょう。

長時間実行されるメソッドは、非同期処理とFutureインターフェイスの候補として適しています。 これにより、Futureにカプセル化されたタスクが完了するのを待っている間に、他のプロセスを実行できます。

Futureの非同期性を利用する操作の例は次のとおりです。

  • 計算集約プロセス(数学および科学計算)

  • 大きなデータ構造(ビッグデータ)の操作

  • リモートメソッド呼び出し(ファイルのダウンロード、HTMLスクレイピング、Webサービス)。

2.1. FutureTaskを使用したFuturesの実装

この例では、Integerの2乗を計算する非常に単純なクラスを作成します。 これは間違いなく「長時間実行」メソッドのカテゴリには当てはまりませんが、Thread.sleep()呼び出しを実行して、最後の1秒間を完了させます。

public class SquareCalculator {

    private ExecutorService executor
      = Executors.newSingleThreadExecutor();

    public Future calculate(Integer input) {
        return executor.submit(() -> {
            Thread.sleep(1000);
            return input * input;
        });
    }
}

実際に計算を実行するコードのビットは、ラムダ式として提供されるcall()メソッドに含まれています。 ご覧のとおり、前述のsleep()呼び出しを除いて、特別なことは何もありません。

CallableExecutorServiceの使用法に注意を向けると、さらに興味深いものになります。

Callableは、結果を返すタスクを表すインターフェイスであり、単一のcall()メソッドがあります。 ここでは、ラムダ式を使用してそのインスタンスを作成しました。

Callableのインスタンスを作成しても、どこにも移動しません。このインスタンスをエグゼキュータに渡して、新しいスレッドでそのタスクを開始し、貴重なFutureオブジェクトを返す必要があります。 そこで、ExecutorServiceが登場します。

ExecutorServiceインスタンスを取得する方法はいくつかありますが、それらのほとんどはユーティリティクラスExecutorsの静的ファクトリメソッドによって提供されます。 この例では、基本的なnewSingleThreadExecutor()を使用しました。これにより、一度に1つのスレッドを処理できるExecutorServiceが得られます。

ExecutorServiceオブジェクトを取得したら、Callableを引数として渡してsubmit()を呼び出す必要があります。 submit()はタスクの開始を処理し、Futureインターフェイスの実装であるFutureTaskオブジェクトを返します。

3. Futuresの消費

ここまで、Futureのインスタンスを作成する方法を学びました。

このセクションでは、FutureのAPIの一部であるすべてのメソッドを調べて、このインスタンスを操作する方法を学習します。

3.1. isDone()get()を使用して結果を取得する

次に、calculate()を呼び出し、返されたFutureを使用して、結果のIntegerを取得する必要があります。 Future APIの2つのメソッドは、このタスクに役立ちます。

Future.isDone()は、エグゼキュータがタスクの処理を終了したかどうかを示します。 タスクが完了すると、trueが返されます。それ以外の場合は、falseが返されます。

計算から実際の結果を返すメソッドはFuture.get()です。 このメソッドはタスクが完了するまで実行をブロックすることに注意してください。ただし、この例では、isDone()を呼び出してタスクが完了したかどうかを最初に確認するため、これは問題になりません。

これらの2つのメソッドを使用することで、メインタスクの完了を待つ間に他のコードを実行できます。

Future future = new SquareCalculator().calculate(10);

while(!future.isDone()) {
    System.out.println("Calculating...");
    Thread.sleep(300);
}

Integer result = future.get();

この例では、プログラムが計算を実行していることをユーザーに知らせるために、出力に簡単なメッセージを書き込みます。

メソッドget()は、タスクが完了するまで実行をブロックします。 ただし、この例では、タスクが終了したことを確認した後でget()が呼び出されるポイントにしか到達しないため、これについて心配する必要はありません。 したがって、このシナリオでは、future.get()は常にすぐに戻ります。

get()には、タイムアウトとTimeUnitを引数として取るオーバーロードバージョンがあることに言及する価値があります。

Integer result = future.get(500, TimeUnit.MILLISECONDS);

get(long, TimeUnit)get()の違いは、指定されたタイムアウト期間の前にタスクが戻らない場合、前者はTimeoutExceptionをスローすることです。

3.2. cancel()Futureをキャンセルする

タスクをトリガーしたが、何らかの理由で結果を気にしなくなったとします。 Future.cancel(boolean)を使用して、エグゼキュータに操作を停止し、その基になるスレッドを中断するように指示できます。

Future future = new SquareCalculator().calculate(4);

boolean canceled = future.cancel(true);

上記のコードのFutureのインスタンスは、操作を完了しません。 実際、そのインスタンスからget()を呼び出そうとすると、cancel()を呼び出した後、結果はCancellationExceptionになります。 Future.isCancelled()は、Futureがすでにキャンセルされているかどうかを通知します。 これは、CancellationExceptionの取得を回避するのに非常に役立ちます。

cancel()の呼び出しが失敗する可能性があります。 その場合、戻り値はfalseになります。 cancel()は引数としてboolean値をとることに注意してください。これは、このタスクを実行するスレッドを中断するかどうかを制御します。

4. Threadプールを使用したその他のマルチスレッド

現在のExecutorServiceは、Executors.newSingleThreadExecutorで取得されているため、シングルスレッドです。 この「単一のスレッド性」を強調するために、2つの計算を同時にトリガーしましょう。

SquareCalculator squareCalculator = new SquareCalculator();

Future future1 = squareCalculator.calculate(10);
Future future2 = squareCalculator.calculate(100);

while (!(future1.isDone() && future2.isDone())) {
    System.out.println(
      String.format(
        "future1 is %s and future2 is %s",
        future1.isDone() ? "done" : "not done",
        future2.isDone() ? "done" : "not done"
      )
    );
    Thread.sleep(300);
}

Integer result1 = future1.get();
Integer result2 = future2.get();

System.out.println(result1 + " and " + result2);

squareCalculator.shutdown();

次に、このコードの出力を分析しましょう。

calculating square for: 10
future1 is not done and future2 is not done
future1 is not done and future2 is not done
future1 is not done and future2 is not done
future1 is not done and future2 is not done
calculating square for: 100
future1 is done and future2 is not done
future1 is done and future2 is not done
future1 is done and future2 is not done
100 and 10000

プロセスが並行していないことは明らかです。 最初のタスクが完了すると2番目のタスクが開始するだけで、プロセス全体が完了するまでに約2秒かかることに注意してください。

プログラムを本当にマルチスレッドにするには、ExecutorServiceの異なるフレーバーを使用する必要があります。 ファクトリメソッドExecutors.newFixedThreadPool()によって提供されるスレッドプールを使用すると、例の動作がどのように変化するかを見てみましょう。

public class SquareCalculator {

    private ExecutorService executor = Executors.newFixedThreadPool(2);

    //...
}

SquareCalculatorクラスを簡単に変更するだけで、2つの同時スレッドを使用できるエグゼキュータができました。

まったく同じクライアントコードを再度実行すると、次の出力が得られます。

calculating square for: 10
calculating square for: 100
future1 is not done and future2 is not done
future1 is not done and future2 is not done
future1 is not done and future2 is not done
future1 is not done and future2 is not done
100 and 10000

これは今ではずっと良くなっています。 2つのタスクが同時に実行を開始および終了し、プロセス全体が完了するまでに約1秒かかることに注意してください。

スレッドプールを作成するために使用できる他のファクトリメソッドがあります。たとえば、以前に使用したThreadsが使用可能になったときにそれを再利用するExecutors.newCachedThreadPool()や、指定された遅延後にコマンドを実行するようにスケジュールするExecutors.newScheduledThreadPool()などです。 (t3)s

ExecutorServiceの詳細については、サブジェクト専用のarticleをお読みください。

5. オーバーロードForkJoinTask

ForkJoinTaskは、Futureを実装する抽象クラスであり、ForkJoinPool内の少数の実際のスレッドによってホストされる多数のタスクを実行できます。

このセクションでは、ForkJoinPoolの主な特性について簡単に説明します。 このトピックに関する包括的なガイドについては、Guide to the Fork/Join Framework in Javaを確認してください。

次に、ForkJoinTaskの主な特徴は、通常、メインタスクを完了するために必要な作業の一部として新しいサブタスクを生成することです。 fork()を呼び出すことによって新しいタスクを生成し、join(),を使用してすべての結果を収集するため、クラスの名前になります。

ForkJoinTaskを実装する2つの抽象クラスがあります。完了時に値を返すRecursiveTaskと、何も返さないRecursiveActionです。 名前が示すように、これらのクラスは、ファイルシステムナビゲーションや複雑な数学計算などの再帰的なタスクに使用されます。

前の例を拡張して、Integerが与えられると、すべての階乗要素の総平方和を計算するクラスを作成しましょう。 したがって、たとえば、数値4を計算機に渡すと、4²+3²+2²+1²の合計である30から結果が得られます。

まず、RecursiveTaskの具体的な実装を作成し、そのcompute()メソッドを実装する必要があります。 ここで、ビジネスロジックを記述します。

public class FactorialSquareCalculator extends RecursiveTask {

    private Integer n;

    public FactorialSquareCalculator(Integer n) {
        this.n = n;
    }

    @Override
    protected Integer compute() {
        if (n <= 1) {
            return n;
        }

        FactorialSquareCalculator calculator
          = new FactorialSquareCalculator(n - 1);

        calculator.fork();

        return n * n + calculator.join();
    }
}

compute()内にFactorialSquareCalculatorの新しいインスタンスを作成することにより、再帰性を実現する方法に注目してください。 非ブロッキングメソッドであるfork()を呼び出すことにより、ForkJoinPoolにこのサブタスクの実行を開始するように依頼します。

join()メソッドは、その計算の結果を返します。これに、現在アクセスしている数値の2乗を加算します。

次に、実行とスレッド管理を処理するためにForkJoinPoolを作成する必要があります。

ForkJoinPool forkJoinPool = new ForkJoinPool();

FactorialSquareCalculator calculator = new FactorialSquareCalculator(10);

forkJoinPool.execute(calculator);

6. 結論

この記事では、Futureインターフェースの包括的なビューを示し、そのすべてのメソッドにアクセスしました。 また、スレッドプールの能力を活用して、複数の並列操作をトリガーする方法も学びました。 ForkJoinTaskクラス、fork()、およびjoin()の主なメソッドについても簡単に説明しました。

Javaの並列および非同期操作に関する他の多くの優れた記事があります。 Futureインターフェースに密接に関連している3つを次に示します(それらのいくつかはすでに記事で言及されています):

この記事で使用されているソースコードをGitHub repositoryで確認してください。