Leitfaden für java.util.concurrent.Future

1. Überblick

In diesem Artikel erfahren Sie mehr über Future .

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 schließlich nach Abschluss der Verarbeitung in der Future erscheint.

Mal sehen, wie man Methoden schreibt, die eine Future -Instanz erstellen und zurückgeben

Langlaufende Methoden sind gute Kandidaten für die asynchrone Verarbeitung und die Future -Schnittstelle. Dies ermöglicht uns, einen anderen Prozess auszuführen, während wir warten, bis die in Future gekapselte Aufgabe abgeschlossen ist.

Einige Beispiele für Operationen, die die asynchrone Natur von Future nutzen würden, sind:

  • rechenintensive Prozesse (mathematisch und wissenschaftlich)

Berechnungen) ** Bearbeitung großer Datenstrukturen (Big Data)

  • Remote-Methodenaufrufe (Herunterladen von Dateien, HTML-Verschrottung, Webdienste).

2.1. Futures mit FutureTask implementieren

In unserem Beispiel erstellen wir eine sehr einfache Klasse, die das Quadrat eines Integer berechnet. Dies passt definitiv nicht in die Kategorie der "lang laufenden" Methoden, aber wir werden einen Thread.sleep () -Aufruf setzen, damit der Vorgang in 1 Sekunde abgeschlossen wird:

public class SquareCalculator {

    private ExecutorService executor
      = Executors.newSingleThreadExecutor();

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

Der Code, der die Berechnung tatsächlich durchführt, ist in der Methode call () enthalten, die als Lambda-Ausdruck bereitgestellt wird. Wie Sie sehen, gibt es nichts Besonderes außer dem oben erwähnten sleep () -Aufruf.

Interessanter wird es, wenn wir uns auf die Verwendung von Callable und https://docs konzentrieren .oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html[ExecutorService] .

Callable ist eine Schnittstelle, die eine Aufgabe darstellt, die ein Ergebnis zurückgibt und über eine einzige 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 jedoch an einen Executor übergeben, der sich um das Starten dieser Aufgabe in einem neuen Thread kümmert und uns das wertvolle Future -Objekt zurückgibt. Hier kommt ExecutorService ins Spiel.

Es gibt einige Möglichkeiten, eine ExecutorService -Instanz zu erhalten, die meisten werden von der Dienstklasse https bereitgestellt://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executors.html Executors]' statische Werksmethoden. In diesem Beispiel haben wir die grundlegende newSingleThreadExecutor () verwendet, die uns einen ExecutorService gibt, mit dem ein einzelner Thread gleichzeitig bearbeitet werden kann.

Sobald wir ein ExecutorService -Objekt haben, müssen wir nur submit () aufrufen und Callable als Argument übergeben. submit () kümmert sich um den Start der Aufgabe und gibt ein FutureTask -Objekt zurück, bei dem es sich um ein Implementierung der Future -Schnittstelle.

3. Verbrauch von Futures

Bis zu diesem Punkt haben wir gelernt, wie Sie eine Instanz von Future erstellen.

In diesem Abschnitt erfahren Sie, wie Sie mit dieser Instanz arbeiten, indem Sie alle Methoden untersuchen, die Teil von sind

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 von isDone () und get () zum Abrufen von Ergebnissen

Jetzt müssen wir calculate () aufrufen und das zurückgegebene Future verwenden, um den resultierenden Integer zu erhalten. Zwei Methoden aus der Future -API helfen uns bei dieser Aufgabe.

Future.isDone () sagt uns, ob der Executor die 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, lautet Future.get () .

Beachten Sie, dass diese Methode die Ausführung blockiert, bis die Task abgeschlossen ist. In unserem Beispiel ist dies jedoch kein Problem, da wir zuerst prüfen, ob die Task abgeschlossen ist, indem Sie isDone () aufrufen.

Mit diesen beiden Methoden können wir anderen Code ausführen, während wir warten, bis die Hauptaufgabe abgeschlossen ist:

Future<Integer> 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 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 keine Sorgen machen, da unser Beispiel erst an den Punkt gelangt, an dem get () aufgerufen wird, nachdem sichergestellt ist, dass die Aufgabe abgeschlossen ist. In diesem Szenario wird future.get () daher immer sofort zurückgegeben.

Erwähnenswert ist, dass get () eine überlastete Version hat, für die ein Timeout und ein TimeUnit as benötigt wird Argumente:

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

Der Unterschied zwischen get (long, TimeUnit) und get () ist, dass der erste einen https wirft://docs.oracle.com/javase/8/docs/api/java/util/concurrent/TimeoutException.html[TimeoutException $ , wenn die Task nicht vor dem angegebenen Zeitlimit zurückgegeben wird.

3.2. Abbrechen eines Future mit cancel ()

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

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

boolean canceled = future.cancel(true);

Unsere Instanz von Future aus dem obigen Code würde niemals ihre Operation abschließen. Wenn wir versuchen, get () von dieser Instanz aus aufzurufen, wäre nach dem Aufruf von cancel () ein https://docs.oracle.com/javase/8/docs/api/java/das Ergebnis. util/concurrent/CancellationException.html[CancellationException] .

Future.isCancelled () sagt uns, ob ein Future bereits abgebrochen wurde. Dies kann sehr nützlich sein, um eine CancellationException zu vermeiden.

Es ist möglich, dass ein Aufruf von cancel () fehlschlägt. In diesem Fall lautet der zurückgegebene Wert false . Beachten Sie, dass cancel () einen boolean -Wert als Argument verwendet - dies steuert, ob der Thread, der diese Task ausführt, unterbrochen werden soll oder nicht.

4. Mehr Multithreading mit Thread Pools

Unser aktueller ExecutorService ist ein einzelner Thread, da er mit https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executors.html#newSingleThreadExecutor-- [Executors.

Um diese "Einzelfadigkeit" hervorzuheben, lösen wir zwei Berechnungen gleichzeitig aus:

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

Nun analysieren wir die Ausgabe für diesen Code:

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

Es ist klar, dass der Prozess nicht parallel verläuft. Beachten Sie, dass die zweite Aufgabe erst gestartet wird, wenn die erste Aufgabe abgeschlossen ist. Der gesamte Vorgang dauert ca. 2 Sekunden.

Um unser Programm wirklich multithreadig zu machen, sollten wir eine andere Variante von ExecutorService verwenden. Mal sehen, wie sich das Verhalten unseres Beispiels ändert, wenn wir einen Thread-Pool verwenden, der von der Factory-Methode https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executors.html#newFixedThreadPool bereitgestellt wird -int-[Executors.newFixedThreadPool ()] :

public class SquareCalculator {

    private ExecutorService executor = Executors.newFixedThreadPool(2);

   //...
}

Mit einer einfachen Änderung in unserer SquareCalculator -Klasse haben wir jetzt einen Executor, der 2 gleichzeitige Threads verwenden kann.

Wenn wir denselben Clientcode erneut ausführen, erhalten Sie die folgende Ausgabe:

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

Das sieht jetzt viel besser aus. Beachten Sie, wie die beiden Tasks gleichzeitig gestartet und beendet werden. Der gesamte Vorgang dauert etwa 1 Sekunde.

Es gibt andere Factory-Methoden, die zum Erstellen von Thread-Pools verwendet werden können, wie Executors.newCachedThreadPool () , das zuvor verwendete _Thread s wiederverwendet, wenn sie verfügbar sind, und https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executors.html#newScheduledThreadPool-int- , bei dem Befehle nach einer bestimmten Verzögerung ausgeführt werden sollen . _

Weitere Informationen zu ExecutorService finden Sie in unserem Link:/java-executor-service-tutorial[Artikel]zum Thema.

5. Überblick über ForkJoinTask

ForkJoinTask ist eine abstrakte Klasse, die Future implementiert und eine große Anzahl von Aufgaben ausführen kann, die von gehostet werden eine kleine Anzahl aktueller Threads in ForkJoinPool .

In diesem Abschnitt werden wir die wichtigsten Merkmale von ForkJoinPool schnell behandeln. Einen umfassenden Leitfaden zu diesem Thema finden Sie unter folgendem Link:/java-fork-join[Leitfaden für das Fork/Join-Framework in Java].

Das Hauptmerkmal einer ForkJoinTask ist, dass normalerweise neue Unteraufgaben als Teil der Arbeit erzeugt werden, die zum Abschließen ihrer Hauptaufgabe erforderlich ist. Es generiert neue Aufgaben, indem fork () aufgerufen wird und alle Ergebnisse mit https erfasst werden ://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ForkJoinTask.html#join--[join ()], , also der Name der Klasse.

Es gibt zwei abstrakte Klassen, die ForkJoinTask implementieren:

RecursiveTask , das nach Abschluss einen Wert zurückgibt, und __https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/RecursiveAction.html[RecursiveAction(, die nichts zurückgibt. Wie die Namen implizieren, sollen diese Klassen für rekursive Aufgaben verwendet werden, wie zum Beispiel Dateisystemnavigation oder komplexe mathematische Berechnungen.

Lassen Sie uns unser vorheriges Beispiel erweitern, um eine Klasse zu erstellen, die angesichts eines Integer die Summenquadrate für alle ihre faktoriellen Elemente berechnet. Wenn wir zum Beispiel die Zahl 4 an unseren Rechner übergeben, sollten wir das Ergebnis aus der Summe von 42 32 22 12 erhalten, die 30 ist.

Zunächst müssen wir eine konkrete Implementierung von RecursiveTask erstellen und die Methode compute () implementieren. Hier schreiben wir unsere Geschäftslogik:

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

Beachten Sie, wie wir Rekursivität erreichen, indem Sie eine neue Instanz von FactorialSquareCalculator in compute () erstellen. Durch Aufruf von fork () , einer nicht blockierenden Methode, bitten wir ForkJoinPool , die Ausführung dieser Unteraufgabe zu initiieren.

Die Methode join () gibt das Ergebnis dieser Berechnung zurück, zu der wir das Quadrat der Zahl hinzufügen, die wir gerade besuchen.

Jetzt müssen wir nur noch einen ForkJoinPool erstellen, um die Ausführung und die Thread-Verwaltung durchzuführen:

ForkJoinPool forkJoinPool = new ForkJoinPool();

FactorialSquareCalculator calculator = new FactorialSquareCalculator(10);

forkJoinPool.execute(calculator);

6. Fazit

In diesem Artikel hatten wir einen umfassenden Überblick über die Future -Schnittstelle und besuchten alle Methoden. Wir haben auch gelernt, wie Sie die Leistungsfähigkeit von Thread-Pools nutzen können, um mehrere parallele Operationen auszulösen. Die wichtigsten Methoden der ForkJoinTask -Klasse fork () und join () wurden ebenfalls kurz behandelt.

Wir haben viele andere großartige Artikel über parallele und asynchrone Operationen in Java. Hier sind drei von ihnen, die eng mit der Future -Schnittstelle zusammenhängen (einige davon werden bereits im Artikel erwähnt):

Implementierung von Future mit vielen zusätzlichen Funktionen, die in Java 8 eingeführt wurden ** Anleitung zum Gabel/Join-Framework in Java - mehr

über ForkJoinTask haben wir in Abschnitt 5 behandelt ** link:/Java-Executor-Service-Tutorial[Anleitung zu Java

ExecutorService ]- für die ExecutorService -Schnittstelle

Überprüfen Sie den in diesem Artikel verwendeten Quellcode in unserem https://github.com/eugenp/tutorials/tree/master/core-java-concurrency [GitHub-Repository.