1. Start einer asynchronen Aufgabe: supplyAsync und runAsync
Die gebräuchlichste Methode, eine asynchrone Aufgabe zu starten, ist CompletableFuture.supplyAsync. Diese Methode nimmt eine Lambda oder eine Methode entgegen, die ein Ergebnis zurückgibt. Beispiel: Wir wollen das Laden von Daten vom Server simulieren:
import java.util.concurrent.CompletableFuture;
public class Main {
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// Simulation einer langwierigen Operation (z. B. Datei herunterladen)
sleep(1000);
return "Daten vom Server";
});
System.out.println("Aufgabe gestartet!");
// ... hier kann man etwas anderes tun, während die Aufgabe läuft
}
private static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
}
}
runAsync: wenn kein Ergebnis benötigt wird
Wenn Ihre Aufgabe nichts zurückgibt (z. B. nur ins Log schreibt oder eine Benachrichtigung sendet), verwenden Sie runAsync:
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
sleep(500);
System.out.println("Vorgang abgeschlossen!");
});
runAsync gibt immer CompletableFuture<Void> zurück, weil kein Ergebnis vorgesehen ist.
2. thenApply, thenAccept, thenRun: worin liegt der Unterschied?
Wenn eine asynchrone Aufgabe beendet ist, möchte man üblicherweise etwas mit dem Ergebnis tun. Dafür gibt es die „Handler“-Methoden:
- thenApply – transformiert das Ergebnis und gibt ein neues Ergebnis zurück.
- thenAccept – nimmt das Ergebnis entgegen, gibt nichts zurück (für Nebeneffekte gedacht).
- thenRun – nimmt kein Ergebnis entgegen und gibt nichts zurück (führt einfach eine Aktion nach Abschluss der Aufgabe aus).
thenApply: Verarbeitung und Transformation des Ergebnisses
Wenn Sie das Ergebnis der vorherigen Aufgabe transformieren möchten, verwenden Sie thenApply. Beispiel: Wir haben einen String geladen und wollen nun seine Länge wissen:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Java");
CompletableFuture<Integer> lengthFuture = future.thenApply(s -> {
System.out.println("Länge des Strings wird berechnet...");
return s.length();
});
// lengthFuture enthält jetzt ein Integer - die Länge des Strings "Java"
lengthFuture.thenAccept(len -> System.out.println("Länge: " + len));
Was passiert:
- future enthält den String "Java".
- thenApply transformiert den String in seine Länge (int).
- thenAccept gibt das Ergebnis aus.
thenAccept: Aktion mit dem Ergebnis (gibt nichts zurück)
Wenn Sie einfach etwas mit dem Ergebnis machen möchten (z. B. es ausgeben), ohne etwas zurückzugeben – verwenden Sie thenAccept:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hallo, Welt!");
future.thenAccept(result -> {
System.out.println("Ergebnis: " + result);
});
thenAccept ist wie ein „Consumer“: Er verbraucht das Ergebnis und tut etwas Nützliches damit.
thenRun: Aktion ohne Ergebnis
Wenn Sie nach Abschluss der Aufgabe einfach eine Aktion ausführen möchten, das Ergebnis aber nicht benötigen, verwenden Sie thenRun:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Fertig!");
future.thenRun(() -> {
System.out.println("Laden abgeschlossen!");
});
Beachten Sie: In thenRun können Sie das Ergebnis der vorherigen Aufgabe nicht erhalten – es wird schlicht ignoriert.
3. Aufrufketten: wir bauen eine Pipeline aus Aufgaben
Die größte Stärke von CompletableFuture ist der Aufbau von Berechnungsketten. Jede Methode (thenApply, thenAccept, thenRun) gibt ein neues CompletableFuture zurück, an das man erneut einen Handler anhängen kann.
Beispiel: mehrstufige Verarbeitung
Erweitern wir unsere Anwendung: Daten laden, sie transformieren, das Ergebnis ausgeben und ins Log schreiben, dass alles beendet ist.
CompletableFuture.supplyAsync(() -> {
System.out.println("Schritt 1: Daten werden geladen...");
sleep(500);
return "Java";
})
.thenApply(data -> {
System.out.println("Schritt 2: Daten werden transformiert...");
return data.toUpperCase();
})
.thenAccept(result -> {
System.out.println("Schritt 3: Ergebnis ausgeben: " + result);
})
.thenRun(() -> {
System.out.println("Schritt 4: Alles abgeschlossen!");
});
Konsolenausgabe:
Schritt 1: Daten werden geladen...
Schritt 2: Daten werden transformiert...
Schritt 3: Ergebnis ausgeben: JAVA
Schritt 4: Alles abgeschlossen!
Beachten Sie:
Jeder nächste Schritt beginnt erst nach Abschluss des vorherigen. So lassen sich echte „Pipelines“ zur Datenverarbeitung bauen.
4. Asynchrone Varianten: thenApplyAsync, thenAcceptAsync, thenRunAsync
Standardmäßig werden die Handler (thenApply, thenAccept, thenRun) in dem Thread ausgeführt, in dem die vorherige Aufgabe beendet wurde. Das ist nicht immer bequem – ist die Verarbeitung schwergewichtig, bringt man sie besser in einen separaten Thread.
Dafür gibt es die asynchronen Varianten:
- thenApplyAsync
- thenAcceptAsync
- thenRunAsync
Worin liegt der Unterschied?
- Ohne Async: Der Handler kann im selben Thread ausgeführt werden wie die vorherige Aufgabe (z. B. wenn die Aufgabe im ForkJoinPool beendet wurde, dann auch der Handler dort).
- Mit Async: Der Handler wird garantiert in einem anderen Thread aus dem ForkJoinPool (oder Ihrem Executor) ausgeführt.
Beispiel: normalen und asynchronen Handler vergleichen
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
System.out.println("Laden... [" + Thread.currentThread().getName() + "]");
return "Hello";
});
future.thenApply(result -> {
System.out.println("thenApply: [" + Thread.currentThread().getName() + "]");
return result + " World";
});
future.thenApplyAsync(result -> {
System.out.println("thenApplyAsync: [" + Thread.currentThread().getName() + "]");
return result + " Async World";
});
Typische Ausgabe:
Laden... [ForkJoinPool.commonPool-worker-1]
thenApply: [ForkJoinPool.commonPool-worker-1]
thenApplyAsync: [ForkJoinPool.commonPool-worker-2]
Fazit:
Der asynchrone Handler wird in einem anderen Thread ausgeführt.
Wann sollte man die Async-Methoden verwenden?
- Wenn die Verarbeitung ressourcenintensiv ist (z. B. aufwendige Berechnungen, Netzwerkarbeit).
- Wenn Sie den Thread nicht blockieren möchten, in dem die vorherige Aufgabe beendet wurde.
- Wenn Sie die Threads explizit steuern möchten (z. B. einen eigenen Executor als zweites Argument übergeben).
5. Nützliche Details
Tabelle: Vergleich der Methoden thenApply, thenAccept, thenRun
| Methode | Verwendet das Ergebnis? | Gibt einen Wert zurück? | Wofür verwenden |
|---|---|---|---|
|
Ja | Ja | Transformation des Ergebnisses |
|
Ja | Nein | Nebeneffekte (Ausgabe, Logging) |
|
Nein | Nein | Nur eine Aktion nach Abschluss der Aufgabe |
|
Ja | Ja | Dasselbe, aber in einem anderen Thread |
|
Ja | Nein | Dasselbe, aber in einem anderen Thread |
|
Nein | Nein | Dasselbe, aber in einem anderen Thread |
Frage: Wie baut man lange Ketten?
Man kann die Methoden nacheinander aufrufen – wie mit LEGO‑Bausteinen:
CompletableFuture.supplyAsync(() -> "42")
.thenApply(Integer::parseInt)
.thenApply(x -> x * 2)
.thenAccept(x -> System.out.println("Ergebnis: " + x));
Ausgabe:
Ergebnis: 84
Jeder nächste Schritt erhält das Ergebnis des vorherigen, kann es ändern oder einfach verwenden.
6. Typische Fehler im Umgang mit thenApply, thenAccept, thenRun
Fehler Nr. 1: Verwechslung der Rückgabetypen.
thenApply muss einen Wert zurückgeben, der in der Kette weitergegeben wird. Wenn Sie versehentlich thenApply verwenden, aber kein Ergebnis zurückgeben, erhält die nächste Operation null (oder es kompiliert gar nicht). Für Nebeneffekte verwenden Sie thenAccept oder thenRun.
Fehler Nr. 2: Versuch, das Ergebnis in thenRun zu verwenden.
In thenRun gibt es keinen Zugriff auf das Ergebnis der vorherigen Aufgabe. Wenn Sie das Ergebnis verwenden möchten, wählen Sie thenApply oder thenAccept.
Fehler Nr. 3: Blockieren des Haupt‑Threads.
Wenn Sie get() oder join() im Haupt‑Thread aufrufen, verlieren Sie die Vorteile der Asynchronität: Der Thread wartet auf den Abschluss der Aufgabe – wie im guten alten synchronen Code. Besser sind nicht blockierende Ketten und Callbacks.
Fehler Nr. 4: Fehlende Fehlerbehandlung.
Wenn in der Kette eine Exception auftritt und Sie keinen Handler hinzugefügt haben (exceptionally, handle, whenComplete), „geht sie verloren“ und die Aufgabe kann mit einem Fehler enden, den Sie nicht sehen. Behandeln Sie in Ketten immer Fehler.
Fehler Nr. 5: Unerwartete Ausführung in einem anderen Thread.
Asynchrone Methoden (thenApplyAsync u. a.) können in einem anderen Thread ausgeführt werden. Wenn Sie auf Variablen zugreifen, die nicht gegen nebenläufigen Zugriff geschützt sind, kann es zu Race Conditions kommen.
GO TO FULL VERSION