java.util.concurrent.Futureのガイド

1。概要

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

2.先物の作成

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

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

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

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

  • 計算集約型プロセス(数学的および科学的計算)
  • 大きなデータ構造(ビッグデータ)の操作
  • リモートメソッド呼び出し(ファイルのダウンロード、HTMLスクレイピング、Webサービス)。

2.1。実装先物をしてFutureTask

この例では、整数の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.先物の消費

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

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

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

次に、calculate()を呼び出し、返されたFutureを使用して、結果の整数を取得する必要があります。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。キャンセル今後のWをi番目の)(キャンセル

タスクをトリガーしたが、何らかの理由で結果を気にしなくなったとします。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()は、引数としてブール値をとることに注意してください。これは、このタスクを実行するスレッドを中断するかどうかを制御します。

4.スレッドプールを使用したマルチスレッドの増加

現在の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

It is clear that the process is not parallel. Notice how the second task only starts once the first task is completed, making the whole process take around 2 seconds to finish.

To make our program really multi-threaded we should use a different flavor of ExecutorService. Let's see how the behavior of our example changes if we use a thread pool, provided by the factory method Executors.newFixedThreadPool():

public class SquareCalculator { private ExecutorService executor = Executors.newFixedThreadPool(2); //... }

With a simple change in our SquareCalculator class now we have an executor which is able to use 2 simultaneous threads.

If we run the exact same client code again, we'll get the following output:

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

This is looking much better now. Notice how the 2 tasks start and finish running simultaneously, and the whole process takes around 1 second to complete.

There are other factory methods that can be used to create thread pools, like Executors.newCachedThreadPool() that reuses previously used Threads when they are available, and Executors.newScheduledThreadPool() which schedules commands to run after a given delay.

For more information about ExecutorService, read our article dedicated to the subject.

5. Overview of ForkJoinTask

ForkJoinTask is an abstract class which implements Future and is capable of running a large number of tasks hosted by a small number of actual threads in ForkJoinPool.

In this section, we are going to quickly cover the main characteristics of ForkJoinPool. For a comprehensive guide about the topic, check our Guide to the Fork/Join Framework in Java.

Then the main characteristic of a ForkJoinTask is that it usually will spawn new subtasks as part of the work required to complete its main task. It generates new tasks by calling fork() and it gathers all results with join(), thus the name of the class.

There are two abstract classes that implement ForkJoinTask: RecursiveTask which returns a value upon completion, and RecursiveAction which doesn't return anything. As the names imply, those classes are to be used for recursive tasks, like for example file-system navigation or complex mathematical computation.

Let's expand our previous example to create a class that, given an Integer, will calculate the sum squares for all its factorial elements. So, for instance, if we pass the number 4 to our calculator, we should get the result from the sum of 4² + 3² + 2² + 1² which is 30.

First of all, we need to create a concrete implementation of RecursiveTask and implement its compute() method. This is where we'll write our business logic:

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(); } }

Notice how we achieve recursiveness by creating a new instance of FactorialSquareCalculator within compute(). By calling fork(), a non-blocking method, we ask ForkJoinPool to initiate the execution of this subtask.

The join() method will return the result from that calculation, to which we add the square of the number we are currently visiting.

Now we just need to create a ForkJoinPool to handle the execution and thread management:

ForkJoinPool forkJoinPool = new ForkJoinPool(); FactorialSquareCalculator calculator = new FactorialSquareCalculator(10); forkJoinPool.execute(calculator);

6. Conclusion

In this article, we had a comprehensive view of the Future interface, visiting all its methods. We've also learned how to leverage the power of thread pools to trigger multiple parallel operations. The main methods from the ForkJoinTask class, fork() and join() were briefly covered as well.

We have many other great articles on parallel and asynchronous operations in Java. Here are three of them that are closely related to the Future interface (some of them are already mentioned in the article):

  • Guide to CompletableFuture – an implementation of Future with many extra features introduced in Java 8
  • JavaのFork / Joinフレームワークのガイド–セクション5で説明したForkJoinTaskの詳細
  • Java ExecutorServiceのガイド–ExecutorServiceインターフェース専用

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