Java Callable und Future
Rückgabewerte mit Callable und Future: warten, Timeout, abbrechen und Ausnahmen weitergeben.
Runnable lässt einen Thread Arbeit verrichten. Es ermöglicht jedoch nicht, einen Wert zurückzugeben oder eine geprüfte Ausnahme zu werfen. Das Paar, das dies ermöglicht, ist Callable<V> (der Produzent) und Future<V> (der Konsument). Man übergibt ein Callable<V> an einen ExecutorService und erhält ein Future<V> zurück — ein Handle zum: Warten auf das Ergebnis, Lesen des Wertes, Abfangen der Aufgaben-Ausnahme oder Abbrechen.
Dies ist die unterste ergebnisbewusste API im gleichzeitigen Toolkit von Java. Das nächste Kapitel, CompletableFuture, fügt Ketten, Kombinatoren und Pipelines hinzu; der Vertrag — "ein asynchrones Ergebnis, auf das man warten kann" — wurde jedoch zuerst von Future definiert, und es ist nach wie vor das richtige Werkzeug für einfaches "tu das und sag mir, wann es fertig ist."
Callable<V> — Runnable mit Rückgabetyp
Das Interface:
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}Die zwei Unterschiede zu Runnable:
- Gibt
Vzurück (den Typparameter). - Darf jede
Exceptionwerfen — einschließlich geprüfter Ausnahmen.
Wie Runnable ist es ein Functional Interface — Lambdas und Methodenreferenzen funktionieren:
Callable<Integer> compute = () -> {
Thread.sleep(100);
return 42;
};
Callable<String> read = () -> Files.readString(Path.of("config.txt")); // can throw IOException
Callable<List<Order>> query = () -> repo.findAll(); // can throw SQLExceptionCallable ist die richtige Form für jede "tu das und gib mir einen Wert zurück"-Aufgabe. Runnable ist nur dann die richtige Form, wenn man sich genuinen kein Ergebnis kümmert.
Future<V> — das Handle für ein asynchrones Ergebnis
Wenn man ein Callable<V> per submit übergibt, gibt der Executor ein Future<V> zurück:
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}Fünf Methoden. Drei davon werden häufig verwendet.
get()
Blockiert den aufrufenden Thread, bis die Aufgabe abgeschlossen ist, und gibt dann das Ergebnis zurück:
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> f = pool.submit(() -> { Thread.sleep(100); return 42; });
Integer value = f.get(); // blocks until done; returns 42get() wirft drei Dinge, die behandelt werden müssen:
InterruptedException— der Aufrufer wurde während des Wartens unterbrochen. Standardbehandlung: Interrupt-Flag erneut setzen und weitergeben.ExecutionException— die Aufgabe selbst hat etwas geworfen. Die ursprüngliche Ausnahme ist eingehüllt; Zugriff über.getCause().CancellationException— jemand hatcancel()auf dem Future aufgerufen.
Eine übliche Form:
try {
Integer v = f.get();
} catch (ExecutionException e) {
Throwable cause = e.getCause(); // the real exception the task threw
// ... handle cause ...
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// ... bail out cooperatively ...
}get(timeout, unit)
Wie get(), aber mit einer Frist. Wirft TimeoutException, wenn die Aufgabe nicht rechtzeitig abgeschlossen wird:
try {
Integer v = f.get(500, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
f.cancel(true); // give up; ask the task to stop
throw new ServiceUnavailableException("timed out");
}Dies ist die richtige Form für "Ich rufe ein Backend auf, das in N ms antworten soll; falls nicht, schnell scheitern." Den Catch immer mit einem cancel(true) kombinieren — sonst läuft die Aufgabe weiter im Hintergrund und belegt einen Thread, dessen Ergebnis man nicht mehr benötigt.
cancel(boolean)
Bittet die Aufgabe, anzuhalten:
boolean cancelled = f.cancel(true); // true = interrupt the running threadDas Argument teilt dem Executor mit, ob der Worker-Thread unterbrochen werden soll. Mit true erhält der Worker eine InterruptedException von jedem blockierenden Aufruf (sleep, wait, I/O); mit false ist die Stornierung wirkungslos, wenn die Aufgabe bereits begonnen hat — nur noch nicht gestartete Aufgaben werden aus der Warteschlange entfernt.
cancel ist kooperativ. Eine Aufgabe, die nicht Thread.currentThread().isInterrupted() prüft und keine blockierenden Aufrufe hat, läuft weiter bis zum Ende. Stornierung ist kein Kill-Schalter — es ist eine Anfrage, die die Aufgabe erfüllen muss.
Ausnahmen: die Einwickelregel
Alles, was das Callable wirft, wird in ExecutionException eingehüllt, wenn get aufgerufen wird. Die Ursache ist das ursprüngliche Throwable:
Future<Integer> f = pool.submit(() -> { throw new IOException("nope"); });
try {
f.get();
} catch (ExecutionException e) {
e.getCause(); // IOException("nope")
e.getCause() instanceof IOException; // true
}Zu beachten ist die Asymmetrie: Das Callable könnte eine geprüfte Ausnahme werfen (das throws Exception in seiner Signatur), aber Future.get deklariert nur ExecutionException. Das Einwickeln ermöglicht es einer Signatur, jeden möglichen Fehler zu transportieren.
Die Runnable.submit-Überladung — pool.submit(Runnable) — gibt ein Future<?> zurück, dessen get() bei Erfolg null zurückgibt und trotzdem jede nicht abgefangene RuntimeException des Runnable einwickelt. Das ist der Standardweg, um herauszufinden, dass ein "fire and forget"-Runnable tatsächlich abgestürzt ist.
Grenzen von Future
Future ist ein Einweg-Kanal: man übergibt, wartet und erhält den Wert. Es lässt sich nicht zusammensetzen:
- Man kann nicht sagen "wenn das fertig ist, führe das mit dem Ergebnis aus."
- Man kann nicht sagen "wenn eines dieser N abgeschlossen ist, tue X."
- Man kann nicht sagen "kombiniere die Ergebnisse dieser zwei Futures ohne Blockierung."
Für all das benötigt man CompletableFuture (nächstes Kapitel). Future ist das richtige Werkzeug, wenn:
- Man nur einen Wert von einer einzelnen Aufgabe zurück möchte.
- Man eine API verwendet, die
Futures zurückgibt, und keine Komposition benötigt. - Der einfachste Vertrag ausreicht.
Für modernen Code, der viel asynchrone Komposition vornimmt, überspringt man Future meistens und greift direkt zu CompletableFuture — aber Future ist der Typ, den der Executor-Service nach wie vor von submit zurückgibt, sodass man beides sehen wird.
FutureTask — die Implementierung hinter submit
Die Klasse, die submit antreibt. Man kann sie direkt verwenden:
FutureTask<Integer> task = new FutureTask<>(() -> compute());
new Thread(task).start(); // FutureTask is a Runnable
Integer v = task.get();Die meisten Programme erstellen FutureTask nicht direkt; das Framework übernimmt das. Nützlich ist es, wenn man ein Future und ein Runnable in einem Objekt benötigt — z. B. um es auf etwas anderem als einem ExecutorService zu planen.
Ein ausgearbeitetes Beispiel: übergeben, Timeout, weitergeben
Das folgende Programm übergibt eine langsame Aufgabe, eine schnelle Aufgabe und eine fehlschlagende Aufgabe; demonstriert get, get(timeout), Ausnahmen-Einwickelung und Stornierung.
Was aus dem Lauf zu entnehmen ist:
- Abschnitt 1 zeigt die einfachste Form: ein
Callableübergeben,getaufrufen, den Wert empfangen.getblockierte den Haupt-Thread für die 50 ms, die die Aufgabe benötigte. Das ist alles, wasFuturein seiner Grundform tut — ein typisiertes, blockierendes Handle für ein Ergebnis, das später eintrifft. - Abschnitt 2 zeigte die Timeout-Form. Die langsame Aufgabe hätte 500 ms gelaufen;
get(100, MS)gab nach 100 ms auf und warfTimeoutException. Das nachfolgendecancel(true)unterbrach den laufenden Thread, damit er früh beenden konnte. Ohne das cancel hätte die Aufgabe die verbleibenden 400 ms weitergelaufen — und einen Thread belegt, dessen Ergebnis man nicht mehr benötigte. - Abschnitt 3 zeigte das Einwickeln von Ausnahmen. Das
CallablewarfIOException;get()warf sie erneut innerhalb vonExecutionException.e.getCause()gab das Original zurück. Das ist der universelle Fehlerkanal der API — jedes geprüfte oder ungeprüfte Throw aus dem Rumpf landet hier. - Abschnitt 4 zeigte das Stornieren einer noch nicht gestarteten Aufgabe. Da beide Pool-Threads mit
hog1undhog2beschäftigt waren, saß diequeued-Aufgabe in der Arbeitswarteschlange;cancel(false)entfernte sie, ohne sie jemals auszuführen. Das Aufrufen vonget()auf dem stornierten Future warfCancellationException— ein anderer Fehlermodus als "Aufgabe hat geworfen" (wasExecutionExceptiongewesen wäre). - Abschnitt 5 zeigte
invokeAny. Die schnellste Aufgabe (50 ms) gewann; die anderen beiden wurden vom Executor storniert.invokeAnyist das richtige Werkzeug für redundante Abfragen — mehrere Quellen aufrufen, den ersten Erfolg verwenden, den Rest aufgeben. Es ist der Baustein hinter abgesicherten Anfrage-Mustern in echten Systemen.
Was kommt als Nächstes
Das nächste Kapitel, Java CompletableFuture, stellt die zusammensetzbare asynchrone API vor — thenApply, thenCompose, allOf, anyOf und die Dutzenden von Kombinatoren, die Future von einem Einzel-Ergebnis-Handle zu einer vollständigen reaktiven Pipeline machen.