1. 概要

このチュートリアルでは、Futureについて学習します。 Java 1.5以降に使用されているインターフェイスであり、非同期呼び出しや並行処理を操作する場合に非常に便利です。

2. 先物の作成

簡単に言えば、 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<Integer> 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 オブジェクトを取得したら、 submit()、を呼び出して、Callableを引数として渡す必要があります。 次に、 submit()はタスクを開始し、Futureインターフェイスの実装であるFutureTaskオブジェクトを返します。

3. 先物の消費

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

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

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

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

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

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

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

Future<Integer> 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. Futurecancel()でキャンセルする

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

Future<Integer> future = new SquareCalculator().calculate(4);

boolean canceled = future.cancel(true);

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

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

4. スレッドプールを使用したより多くのマルチスレッド

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

SquareCalculator squareCalculator = new SquareCalculator();

Future<Integer> future1 = squareCalculator.calculate(10);
Future<Integer> 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秒かかることがわかります。

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

ExecutorService の詳細については、このテーマに関する記事を参照してください。

5. ForkJoinTaskの概要

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

このセクションでは、ForkJoinPoolの主な特徴について簡単に説明します。 このトピックに関する包括的なガイドについては、Javaのフォーク/結合フレームワークのガイドを確認してください。

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

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

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

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

public class FactorialSquareCalculator extends RecursiveTask<Integer> {
 
    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リポジトリにあります。