Leitfaden für CompletableFuture

Leitfaden für CompletableFuture

1. Einführung

Dieser Artikel ist eine Anleitung zu den Funktionen und Anwendungsfällen der KlasseCompletableFuture- eingeführt als Verbesserung der Java 8 Concurrency-API.

Weitere Lektüre:

Lauffähig vs. Aufrufbar in Java

Lernen Sie den Unterschied zwischen ausführbaren und aufrufbaren Schnittstellen in Java kennen.

Read more

Anleitung zu java.util.concurrent.Future

Eine Anleitung zu java.util.concurrent.Future mit einer Übersicht über die verschiedenen Implementierungen

Read more

2. Asynchrone Berechnung in Java

Asynchrone Berechnungen sind schwer zu erklären. Normalerweise möchten wir uns eine Berechnung als eine Reihe von Schritten vorstellen. Bei asynchroner Berechnung istactions represented as callbacks tend to be either scattered across the code or deeply nested inside each other. Noch schlimmer wird es, wenn wir Fehler behandeln müssen, die bei einem der Schritte auftreten können.

DieFuture-Schnittstelle wurde in Java 5 hinzugefügt, um als Ergebnis einer asynchronen Berechnung zu dienen, es gab jedoch keine Methoden, um diese Berechnungen zu kombinieren oder mögliche Fehler zu behandeln.

In Java 8, the CompletableFuture class was introduced. Neben der SchnittstelleFuture wurde auch die SchnittstelleCompletionStage implementiert. Diese Schnittstelle definiert den Vertrag für einen asynchronen Berechnungsschritt, der mit anderen Schritten kombiniert werden kann.

CompletableFuture ist gleichzeitig ein Baustein und ein Framework mitabout 50 different methods for composing, combining, executing asynchronous computation steps and handling errors.

Solch eine große API kann überwältigend sein, aber diese fallen meist in mehrere klare und eindeutige Anwendungsfälle.

3. Verwenden vonCompletableFuture als einfachesFuture

Zunächst implementiert die KlasseCompletableFuturedie SchnittstelleFuture, sodass Sieuse it as a Future implementation, but with additional completion logic können.

Sie können beispielsweise eine Instanz dieser Klasse mit einem Konstruktor ohne Argumente erstellen, um ein zukünftiges Ergebnis darzustellen, es an die Verbraucher weitergeben und zu einem späteren Zeitpunkt mit der Methodecompletevervollständigen. Die Verbraucher können die Methodegetverwenden, um den aktuellen Thread zu blockieren, bis dieses Ergebnis bereitgestellt wird.

Im folgenden Beispiel haben wir eine Methode, die eineCompletableFuture-Instanz erstellt, dann eine Berechnung in einem anderen Thread auslöst und dieFuture sofort zurückgibt.

Wenn die Berechnung abgeschlossen ist, vervollständigt die Methode dieFuture, indem sie das Ergebnis für diecomplete-Methode bereitstellt:

public Future calculateAsync() throws InterruptedException {
    CompletableFuture completableFuture
      = new CompletableFuture<>();

    Executors.newCachedThreadPool().submit(() -> {
        Thread.sleep(500);
        completableFuture.complete("Hello");
        return null;
    });

    return completableFuture;
}

Um die Berechnung abzuspalten, verwenden wir dieExecutor-API, die im Artikel“Introduction to Thread Pools in Java” beschrieben ist. Diese Methode zum Erstellen und Vervollständigen vonCompletableFuture kann jedoch zusammen mit jedem Parallelitätsmechanismus oder jeder API verwendet werden einschließlich roher Fäden.

Beachten Sie, dassthe calculateAsync method returns a Future instance.

Wir rufen einfach die Methode auf, empfangen die Instanz vonFutureund rufen die Methode vongetauf, wenn wir bereit sind, das Ergebnis zu blockieren.

Beachten Sie auch, dass die Methodeget einige geprüfte Ausnahmen auslöst, nämlichExecutionException (Kapselung einer Ausnahme, die während einer Berechnung aufgetreten ist) undInterruptedException (eine Ausnahme, die angibt, dass ein Thread, der eine Methode ausführt, unterbrochen wurde). ::

Future completableFuture = calculateAsync();

// ...

String result = completableFuture.get();
assertEquals("Hello", result);

If you already know the result of a computation können Sie die statische MethodecompletedFuture mit einem Argument verwenden, das ein Ergebnis dieser Berechnung darstellt. Dann wird dieget-Methode derFuture niemals blockiert und stattdessen sofort dieses Ergebnis zurückgegeben.

Future completableFuture =
  CompletableFuture.completedFuture("Hello");

// ...

String result = completableFuture.get();
assertEquals("Hello", result);

Als alternatives Szenario möchten Sie möglicherweisecancel the execution of a Future.

Angenommen, wir haben kein Ergebnis gefunden und beschlossen, eine asynchrone Ausführung insgesamt abzubrechen. Dies kann mit derFuturecancel-Methode erfolgen. Diese Methode empfängt einboolean-ArgumentmayInterruptIfRunning, hat jedoch im Fall vonCompletableFuture keine Auswirkung, da Interrupts nicht zur Steuerung der Verarbeitung fürCompletableFuture verwendet werden.

Hier ist eine modifizierte Version der asynchronen Methode:

public Future calculateAsyncWithCancellation() throws InterruptedException {
    CompletableFuture completableFuture = new CompletableFuture<>();

    Executors.newCachedThreadPool().submit(() -> {
        Thread.sleep(500);
        completableFuture.cancel(false);
        return null;
    });

    return completableFuture;
}

Wenn wir das Ergebnis mit der MethodeFuture.get() blockieren, wirdCancellationException ausgelöst, wenn die Zukunft abgebrochen wird:

Future future = calculateAsyncWithCancellation();
future.get(); // CancellationException

4. CompletableFuture mit gekapselter Berechnungslogik

Mit dem obigen Code können wir einen beliebigen Mechanismus für die gleichzeitige Ausführung auswählen, aber was ist, wenn wir dieses Boilerplate überspringen und einfach einen Code asynchron ausführen möchten?

Mit den statischen MethodenrunAsync undsupplyAsync können wir eineCompletableFuture-Instanz aus den FunktionstypenRunnable undSupplierentsprechend erstellen.

SowohlRunnable als auchSupplier sind funktionale Schnittstellen, mit denen ihre Instanzen dank der neuen Java 8-Funktion als Lambda-Ausdrücke übergeben werden können.

DieRunnable-Schnittstelle ist dieselbe alte Schnittstelle, die in Threads verwendet wird, und es ist nicht möglich, einen Wert zurückzugeben.

DieSupplier-Schnittstelle ist eine generische Funktionsschnittstelle mit einer einzelnen Methode, die keine Argumente enthält und einen Wert eines parametrisierten Typs zurückgibt.

Dies ermöglichtprovide an instance of the Supplier as a lambda expression that does the calculation and returns the result. Das ist so einfach wie:

CompletableFuture future
  = CompletableFuture.supplyAsync(() -> "Hello");

// ...

assertEquals("Hello", future.get());

5. Verarbeitungsergebnisse asynchroner Berechnungen

Der allgemeinste Weg, das Ergebnis einer Berechnung zu verarbeiten, besteht darin, es einer Funktion zuzuführen. DiethenApply-Methode macht genau das: Akzeptiert eineFunction-Instanz, verwendet sie zur Verarbeitung des Ergebnisses und gibt einFuture zurück, das einen von einer Funktion zurückgegebenen Wert enthält:

CompletableFuture completableFuture
  = CompletableFuture.supplyAsync(() -> "Hello");

CompletableFuture future = completableFuture
  .thenApply(s -> s + " World");

assertEquals("Hello World", future.get());

Wenn Sie in derFuture-Kette keinen Wert zurückgeben müssen, können Sie eine Instanz der Funktionsschnittstelle vonConsumerverwenden. Die einzelne Methode verwendet einen Parameter und gibtvoid zurück.

FürCompletableFuture gibt es eine Methode für diesen Anwendungsfall. Die MethodethenAccept empfängtConsumer und übergibt sie als Ergebnis der Berechnung. Der letzte Aufruf vonfuture.get()gibt eine Instanz vom TypVoidzurück.

CompletableFuture completableFuture
  = CompletableFuture.supplyAsync(() -> "Hello");

CompletableFuture future = completableFuture
  .thenAccept(s -> System.out.println("Computation returned: " + s));

future.get();

Wenn Sie weder den Wert der Berechnung benötigen noch am Ende der Kette einen Wert zurückgeben möchten, können Sie einRunnable-Lambda an diethenRun-Methode übergeben. Im folgenden Beispiel wird nach dem Aufruf der Methodefuture.get() einfach eine Zeile in der Konsole gedruckt:

CompletableFuture completableFuture
  = CompletableFuture.supplyAsync(() -> "Hello");

CompletableFuture future = completableFuture
  .thenRun(() -> System.out.println("Computation finished."));

future.get();

6. Futures kombinieren

Der beste Teil derCompletableFuture-API istability to combine CompletableFuture instances in a chain of computation steps.

Das Ergebnis dieser Verkettung ist selbst einCompletableFuture, das eine weitere Verkettung und Kombination ermöglicht. Dieser Ansatz ist in funktionalen Sprachen allgegenwärtig und wird oft als monadisches Entwurfsmuster bezeichnet.

Im folgenden Beispiel verwenden wir diethenCompose-Methode, um zweiFutures nacheinander zu verketten.

Beachten Sie, dass diese Methode eine Funktion verwendet, die die Instanz vonCompletableFuturezurückgibt. Das Argument dieser Funktion ist das Ergebnis des vorherigen Berechnungsschritts. Dies ermöglicht es uns, diesen Wert innerhalb des Lambda des nächstenCompletableFuturezu verwenden:

CompletableFuture completableFuture
  = CompletableFuture.supplyAsync(() -> "Hello")
    .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + " World"));

assertEquals("Hello World", completableFuture.get());

Die MethodethenCompose implementiert zusammen mitthenApply grundlegende Bausteine ​​des monadischen Musters. Sie stehen in enger Beziehung zu den Methodenmap undflatMap der KlassenStream undOptional, die auch in Java 8 verfügbar sind.

Beide Methoden erhalten eine Funktion und wenden sie auf das Berechnungsergebnis an, jedoch diethenCompose (flatMap) -Methodereceives a function that returns another object of the same type. Diese Funktionsstruktur ermöglicht es, die Instanzen dieser Klassen als Bausteine ​​zusammenzustellen.

Wenn Sie zwei unabhängigeFutures ausführen und etwas mit ihren Ergebnissen tun möchten, verwenden Sie diethenCombine-Methode, dieFuture undFunction mit zwei Argumenten akzeptiert, um beide Ergebnisse zu verarbeiten:

CompletableFuture completableFuture
  = CompletableFuture.supplyAsync(() -> "Hello")
    .thenCombine(CompletableFuture.supplyAsync(
      () -> " World"), (s1, s2) -> s1 + s2));

assertEquals("Hello World", completableFuture.get());

Ein einfacherer Fall ist, wenn Sie etwas mit zweiFutures-Ergebnissen tun möchten, aber keinen resultierenden Wert an eineFuture-Kette weitergeben müssen. DiethenAcceptBoth-Methode hilft dabei:

CompletableFuture future = CompletableFuture.supplyAsync(() -> "Hello")
  .thenAcceptBoth(CompletableFuture.supplyAsync(() -> " World"),
    (s1, s2) -> System.out.println(s1 + s2));

7. Differenz zwischenthenApply() undthenCompose()

In unseren vorherigen Abschnitten haben wir Beispiele fürthenApply() undthenCompose() gezeigt. Beide APIs helfen dabei, unterschiedlicheCompletableFuture-Aufrufe zu verketten, aber die Verwendung dieser beiden Funktionen ist unterschiedlich.

7.1. thenApply()

This method is used for working with a result of the previous call. Ein wichtiger Punkt ist jedoch, dass der Rückgabetyp aus allen Aufrufen kombiniert wird.

Diese Methode ist also nützlich, wenn wir das Ergebnis einesCompletableFuture -Scall transformieren möchten:

CompletableFuture finalResult = compute().thenApply(s-> s + 1);

7.2. thenCompose()

Die MethodethenCompose() ähneltthenApply() darin, dass beide eine neue Abschlussstufe zurückgeben. thenCompose() uses the previous stage as the argument. Es wird abgeflacht und gibtFuture mit dem Ergebnis direkt zurück, anstatt eine verschachtelte Zukunft, wie wir inthenApply(): beobachtet haben

CompletableFuture computeAnother(Integer i){
    return CompletableFuture.supplyAsync(() -> 10 + i);
}
CompletableFuture finalResult = compute().thenCompose(this::computeAnother);

Wenn Sie alsoCompletableFuture Methoden verketten möchten, ist es besser,thenCompose() zu verwenden.

Beachten Sie auch, dass der Unterschied zwischen diesen beiden Methoden analog zuhttps://www.example.com/java-difference-map-and-flatmap. ist

8. Paralleles Ausführen mehrererFutures

Wenn wir mehrereFutures parallel ausführen müssen, möchten wir normalerweise warten, bis alle ausgeführt sind, und dann ihre kombinierten Ergebnisse verarbeiten.

Die statische Methode vonCompletableFuture.allOfermöglicht es, auf den Abschluss aller als var-arg bereitgestelltenFutureszu warten:

CompletableFuture future1
  = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture future2
  = CompletableFuture.supplyAsync(() -> "Beautiful");
CompletableFuture future3
  = CompletableFuture.supplyAsync(() -> "World");

CompletableFuture combinedFuture
  = CompletableFuture.allOf(future1, future2, future3);

// ...

combinedFuture.get();

assertTrue(future1.isDone());
assertTrue(future2.isDone());
assertTrue(future3.isDone());

Beachten Sie, dass der Rückgabetyp vonCompletableFuture.allOf()CompletableFuture<Void> ist. Die Einschränkung dieser Methode besteht darin, dass nicht die kombinierten Ergebnisse allerFutures zurückgegeben werden. Stattdessen müssen Sie die Ergebnisse vonFutures manuell abrufen. Glücklicherweise macht es die Methode vonCompletableFuture.join()und die Java 8 Streams-API einfach:

String combined = Stream.of(future1, future2, future3)
  .map(CompletableFuture::join)
  .collect(Collectors.joining(" "));

assertEquals("Hello Beautiful World", combined);

Die MethodeCompletableFuture.join() ähnelt der Methodeget, löst jedoch eine ungeprüfte Ausnahme aus, fallsFuture nicht normal abgeschlossen wird. Dies ermöglicht die Verwendung als Methodenreferenz in der MethodeStream.map().

9. Fehler behandeln

Für die Fehlerbehandlung in einer Kette von asynchronen Berechnungsschritten musste das Idiom vonthrow/catchauf ähnliche Weise angepasst werden.

Anstatt eine Ausnahme in einem syntaktischen Block abzufangen, können Sie sie mit der KlasseCompletableFuturein einer speziellenhandle-Methode behandeln. Diese Methode empfängt zwei Parameter: ein Ergebnis einer Berechnung (wenn sie erfolgreich abgeschlossen wurde) und eine Ausnahme (wenn ein Berechnungsschritt nicht normal abgeschlossen wurde).

Im folgenden Beispiel verwenden wir die Methodehandle, um einen Standardwert bereitzustellen, wenn die asynchrone Berechnung einer Begrüßung mit einem Fehler abgeschlossen wurde, da kein Name angegeben wurde:

String name = null;

// ...

CompletableFuture completableFuture
  =  CompletableFuture.supplyAsync(() -> {
      if (name == null) {
          throw new RuntimeException("Computation error!");
      }
      return "Hello, " + name;
  })}).handle((s, t) -> s != null ? s : "Hello, Stranger!");

assertEquals("Hello, Stranger!", completableFuture.get());

Nehmen wir als alternatives Szenario an, wir möchtenFuture wie im ersten Beispiel manuell mit einem Wert vervollständigen, aber auch die Möglichkeit haben, ihn mit einer Ausnahme zu vervollständigen. Dafür ist die MethodecompleteExceptionallyvorgesehen. Die MethodecompletableFuture.get() im folgenden Beispiel löst einExecutionException mit einemRuntimeException als Ursache aus:

CompletableFuture completableFuture = new CompletableFuture<>();

// ...

completableFuture.completeExceptionally(
  new RuntimeException("Calculation failed!"));

// ...

completableFuture.get(); // ExecutionException

Im obigen Beispiel hätten wir die Ausnahme mit der Methodehandle asynchron behandeln können, aber mit der Methodegetkönnen wir einen typischeren Ansatz für eine synchrone Ausnahmeverarbeitung verwenden.

10. Asynchrone Methoden

Die meisten Methoden der fließenden API in der KlasseCompletableFuturehaben zwei zusätzliche Varianten mit dem PostfixAsync. Diese Methoden sind normalerweise fürrunning a corresponding step of execution in another thread vorgesehen.

Die Methoden ohne das PostfixAsyncführen die nächste Ausführungsstufe mit einem aufrufenden Thread aus. Die MethodeAsync ohne das ArgumentExecutor führt einen Schritt unter Verwendung der allgemeinen Poolimplementierung vonfork/join vonExecutor aus, auf die mit der MethodeForkJoinPool.commonPool() zugegriffen wird. Die MethodeAsync mit dem ArgumentExecutor führt einen Schritt unter Verwendung der übergebenenExecutor aus.

Hier ist ein modifiziertes Beispiel, das das Ergebnis einer Berechnung mit einerFunction-Instanz verarbeitet. Der einzige sichtbare Unterschied ist diethenApplyAsync-Methode. Unter der Haube wird die Anwendung einer Funktion jedoch in eineForkJoinTask-Instanz eingeschlossen (weitere Informationen zumfork/join-Strahmen finden Sie im Artikel“Guide to the Fork/Join Framework in Java”). Dies ermöglicht eine noch stärkere Parallelisierung Ihrer Berechnung und eine effizientere Nutzung der Systemressourcen.

CompletableFuture completableFuture
  = CompletableFuture.supplyAsync(() -> "Hello");

CompletableFuture future = completableFuture
  .thenApplyAsync(s -> s + " World");

assertEquals("Hello World", future.get());

11. JDK 9CompletableFuture API

In Java 9 wurde die API vonCompletableFuturemit den folgenden Änderungen weiter verbessert:

  • Neue Factory-Methoden hinzugefügt

  • Unterstützung für Verzögerungen und Zeitüberschreitungen

  • Verbesserte Unterstützung für Unterklassen.

Neue Instanz-APIs wurden eingeführt:

  • Executor defaultExecutor ()

  • CompletableFuture newIncompleteFuture ()

  • CompletableFuture copy ()

  • CompletionStage minimalCompletionStage ()

  • CompletableFuture completeAsync (Supplier Lieferant, Executor Executor)

  • CompletableFuture completeAsync (Supplier Lieferant)

  • CompletableFuture oder Timeout (lange Zeitüberschreitung, TimeUnit-Einheit)

  • CompletableFuture completeOnTimeout (T-Wert, lange Zeitüberschreitung, TimeUnit-Einheit)

Wir haben jetzt auch einige statische Dienstprogrammmethoden:

  • Executor verzögertExecutor (lange Verzögerung, TimeUnit-Einheit, Executor Executor)

  • Executor verzögertExecutor (lange Verzögerung, TimeUnit-Einheit)

  • CompletionStage completeStage (U-Wert)

  • CompletionStage failedStage (Throwable ex)

  • CompletableFuture failedFuture (Throwable ex)

Um das Timeout zu beheben, hat Java 9 zwei weitere neue Funktionen eingeführt:

  • orTimeout ()

  • completeOnTimeout ()

Hier ist der ausführliche Artikel zur weiteren Lektüre:Java 9 CompletableFuture API Improvements.

12. Fazit

In diesem Artikel haben wir die Methoden und typischen Anwendungsfälle derCompletableFuture-Klasse beschrieben.

Der Quellcode für den Artikel istover on GitHub verfügbar.