Anleitung zu java.util.concurrent.Future

1. Übersicht

In diesem Artikel lernen wir die Zukunft kennen . Eine Schnittstelle, die es seit Java 1.5 gibt und die bei asynchronen Aufrufen und gleichzeitiger Verarbeitung sehr nützlich sein kann.

2. Futures erstellen

Einfach ausgedrückt stellt die Future- Klasse ein zukünftiges Ergebnis einer asynchronen Berechnung dar - ein Ergebnis, das nach Abschluss der Verarbeitung möglicherweise in der Zukunft angezeigt wird.

Mal sehen, wie Methoden geschrieben werden, die eine Future- Instanz erstellen und zurückgeben .

Lang laufende Methoden sind gute Kandidaten für die asynchrone Verarbeitung und die Future- Schnittstelle. Auf diese Weise können wir einen anderen Prozess ausführen, während wir auf den Abschluss der in Future gekapselten Aufgabe warten .

Einige Beispiele für Operationen, die die Asynchronität von Future nutzen würden, sind:

  • rechenintensive Prozesse (mathematische und wissenschaftliche Berechnungen)
  • Manipulieren großer Datenstrukturen (Big Data)
  • Remote-Methodenaufrufe (Herunterladen von Dateien, HTML-Scrapping, Webdienste).

2.1. Die Implementierung Futures Mit FutureTask

In unserem Beispiel erstellen wir eine sehr einfache Klasse, die das Quadrat einer Ganzzahl berechnet . Dies passt definitiv nicht in die Kategorie der "lang laufenden" Methoden, aber wir werden einen Thread.sleep () -Aufruf darauf setzen, damit es 1 Sekunde dauert , bis es abgeschlossen ist:

public class SquareCalculator { private ExecutorService executor = Executors.newSingleThreadExecutor(); public Future calculate(Integer input) { return executor.submit(() -> { Thread.sleep(1000); return input * input; }); } }

Das Codebit, das die Berechnung tatsächlich ausführt, ist in der call () -Methode enthalten, die als Lambda-Ausdruck bereitgestellt wird. Wie Sie sehen, ist nichts Besonderes daran, außer dem zuvor erwähnten Aufruf von sleep () .

Interessanter wird es, wenn wir unsere Aufmerksamkeit auf die Verwendung von Callable und ExecutorService lenken .

Callable ist eine Schnittstelle, die eine Aufgabe darstellt, die ein Ergebnis zurückgibt und über eine einzelne call () -Methode verfügt. Hier haben wir eine Instanz davon mit einem Lambda-Ausdruck erstellt.

Das Erstellen einer Instanz von Callable führt uns nicht weiter. Wir müssen diese Instanz dennoch an einen Executor übergeben, der sich darum kümmert, diese Aufgabe in einem neuen Thread zu starten und uns das wertvolle Future- Objekt zurückzugeben. Hier kommt ExecutorService ins Spiel .

Es gibt einige Möglichkeiten, wie wir eine ExecutorService- Instanz erhalten können. Die meisten davon werden von den statischen Factory-Methoden der Executors der Utility-Klasse bereitgestellt . In diesem Beispiel haben wir den grundlegenden newSingleThreadExecutor () verwendet , mit dem wir einen ExecutorService erhalten , der jeweils nur einen Thread verarbeiten kann.

Sobald wir ein ExecutorService- Objekt haben, müssen wir nur submit () aufrufen und unser Callable als Argument übergeben. submit () kümmert sich um das Starten der Aufgabe und gibt ein FutureTask- Objekt zurück, das eine Implementierung der Future- Schnittstelle ist.

3. Futures konsumieren

Bis zu diesem Punkt haben wir gelernt, wie man eine Instanz von Future erstellt .

In diesem Abschnitt erfahren Sie, wie Sie mit dieser Instanz arbeiten, indem Sie alle Methoden untersuchen, die Teil der Future -API sind.

3.1. Verwenden Sie isDone () und get (), um Ergebnisse zu erhalten

Jetzt müssen wir berechne () aufrufen und die zurückgegebene Zukunft verwenden , um die resultierende Ganzzahl zu erhalten . Zwei Methoden aus der Future- API helfen uns bei dieser Aufgabe.

Future.isDone () teilt uns mit, ob der Executor die Verarbeitung der Aufgabe abgeschlossen hat. Wenn die Aufgabe abgeschlossen ist, wird true zurückgegeben, andernfalls wird false zurückgegeben .

Die Methode, die das tatsächliche Ergebnis der Berechnung zurückgibt, ist Future.get () . Beachten Sie, dass diese Methode die Ausführung blockiert, bis die Aufgabe abgeschlossen ist. In unserem Beispiel ist dies jedoch kein Problem, da wir zuerst überprüfen, ob die Aufgabe abgeschlossen ist, indem wir isDone () aufrufen .

Mit diesen beiden Methoden können wir einen anderen Code ausführen, während wir auf den Abschluss der Hauptaufgabe warten:

Future future = new SquareCalculator().calculate(10); while(!future.isDone()) { System.out.println("Calculating..."); Thread.sleep(300); } Integer result = future.get();

In diesem Beispiel schreiben wir eine einfache Nachricht in die Ausgabe, um den Benutzer darüber zu informieren, dass das Programm die Berechnung durchführt.

Die Methode get () blockiert die Ausführung, bis die Aufgabe abgeschlossen ist. Aber wir müssen uns darüber keine Sorgen machen, da unser Beispiel erst an den Punkt gelangt, an dem get () aufgerufen wird, nachdem sichergestellt wurde, dass die Aufgabe abgeschlossen ist. In diesem Szenario wird future.get () immer sofort zurückgegeben.

Es ist erwähnenswert, dass get () eine überladene Version hat, die ein Timeout und eine TimeUnit als Argumente verwendet:

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

Der Unterschied zwischen get (long, TimeUnit) und get () besteht darin , dass erstere eine TimeoutException auslösen, wenn die Aufgabe nicht vor dem angegebenen Zeitlimit zurückkehrt.

3.2. Abbrechen einer Zukunft mit Abbrechen ()

Angenommen, wir haben eine Aufgabe ausgelöst, aber aus irgendeinem Grund interessiert uns das Ergebnis nicht mehr. Wir können Future.cancel (boolean) verwenden , um den Executor anzuweisen , die Operation zu stoppen und den zugrunde liegenden Thread zu unterbrechen:

Future future = new SquareCalculator().calculate(4); boolean canceled = future.cancel(true);

Unsere Instanz von Future aus dem obigen Code würde ihren Betrieb niemals abschließen. Wenn wir versuchen, get () von dieser Instanz aus aufzurufen , ist das Ergebnis nach dem Aufruf von cancel () eine CancellationException . Future.isCancelled () teilt uns mit, ob eine Zukunft bereits abgebrochen wurde. Dies kann sehr nützlich sein, um eine CancellationException zu vermeiden .

Möglicherweise schlägt ein Aufruf zum Abbrechen () fehl. In diesem Fall ist der zurückgegebene Wert falsch . Beachten Sie, dass cancel () einen booleschen Wert als Argument verwendet. Hiermit wird gesteuert, ob der Thread, der diese Aufgabe ausführt, unterbrochen werden soll oder nicht.

4. Mehr Multithreading mit Thread- Pools

Unser aktueller ExecutorService ist Single-Threaded, da er mit dem Executors.newSingleThreadExecutor abgerufen wurde. Um diese „Einzelthreadness“ hervorzuheben, lösen wir zwei Berechnungen gleichzeitig aus:

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

Lassen Sie uns nun die Ausgabe für diesen Code analysieren:

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
  • Leitfaden zum Fork / Join-Framework in Java - Weitere Informationen zu ForkJoinTask finden Sie in Abschnitt 5
  • Handbuch zum Java ExecutorService - für die ExecutorService- Schnittstelle

Überprüfen Sie den in diesem Artikel verwendeten Quellcode in unserem GitHub-Repository.