W3docs

Java Callable e Future

Restituisci valori dai task con Callable e consumali in modo asincrono con Future: attesa, timeout, cancellazione e gestione delle eccezioni.

Runnable permette a un thread di svolgere del lavoro, ma non consente di restituire un valore né di lanciare un'eccezione checked. La coppia che lo fa è Callable<V> (il produttore) e Future<V> (il consumatore). Quando si invia un Callable<V> a un ExecutorService, si ottiene in cambio un Future<V>, che rappresenta il riferimento per: attendere il risultato, leggere il valore, catturare l'eccezione del task o annullarlo.

Questa è l'API di più basso livello consapevole dei risultati nel toolkit concorrente di Java. Il capitolo successivo, CompletableFuture, aggiunge catene, combinatori e pipeline; tuttavia il contratto — "un risultato asincrono su cui è possibile attendere" — è stato definito da Future per primo, ed è ancora lo strumento giusto per un semplice "vai, fai questo e dimmi quando hai finito."

Callable<V> — Runnable con tipo di ritorno

L'interfaccia:

@FunctionalInterface
public interface Callable<V> {
  V call() throws Exception;
}

Le due differenze rispetto a Runnable:

  1. Restituisce V (il parametro di tipo).
  2. Può lanciare qualsiasi Exception, incluse le eccezioni checked.

Come Runnable, è un'interfaccia funzionale — lambda e riferimenti a metodi funzionano:

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 SQLException

Callable è la forma giusta per qualsiasi operazione del tipo "vai, fai questo e portami un valore". Runnable è la forma giusta solo quando non si ha bisogno di un risultato.

Future<V> — il riferimento a un risultato asincrono

Quando si esegue la submit di un Callable<V>, l'executor restituisce un Future<V>:

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

Cinque metodi. Tre che si usano spesso.

get()

Blocca il thread chiamante finché il task non è completato, poi restituisce il risultato:

ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> f = pool.submit(() -> { Thread.sleep(100); return 42; });
Integer value = f.get();                              // blocks until done; returns 42

get() lancia tre eccezioni che è necessario gestire:

  • InterruptedException — il chiamante è stato interrotto durante l'attesa. Trattamento standard: reimpostare il flag di interruzione e propagare.
  • ExecutionException — il task stesso ha lanciato qualcosa. L'eccezione originale è incapsulata; vi si accede tramite .getCause().
  • CancellationException — qualcuno ha chiamato cancel() sul future.

Una forma comune:

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)

Uguale a get() ma con una scadenza. Lancia TimeoutException se il task non termina in tempo:

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

Questa è la forma giusta per "sto chiamando un backend che dovrebbe rispondere in N ms; se non risponde, fallisco velocemente." Abbinare sempre il catch a un cancel(true) — altrimenti il task continua a girare in background, occupando un thread del cui risultato non ci interessa più.

cancel(boolean)

Chiede al task di fermarsi:

boolean cancelled = f.cancel(true);                   // true = interrupt the running thread

L'argomento indica all'executor se interrompere il thread worker. Con true, il worker riceve un InterruptedException da qualsiasi chiamata bloccante (sleep, wait, I/O); con false, la cancellazione non ha effetto se il task è già avviato — solo i task non ancora avviati vengono rimossi dalla coda.

cancel è cooperativo. Un task che non controlla Thread.currentThread().isInterrupted() e non ha chiamate bloccanti continuerà a girare fino al termine. La cancellazione non è un kill switch — è una richiesta che il task deve rispettare.

Eccezioni: la regola dell'incapsulamento

Tutto ciò che Callable lancia viene incapsulato in ExecutionException quando si chiama get. La causa è il throwable originale:

Future<Integer> f = pool.submit(() -> { throw new IOException("nope"); });
try {
  f.get();
} catch (ExecutionException e) {
  e.getCause();                                       // IOException("nope")
  e.getCause() instanceof IOException;                // true
}

Si noti l'asimmetria: il Callable potrebbe lanciare un'eccezione checked (il throws Exception nella sua firma), ma Future.get dichiara solo ExecutionException. L'incapsulamento è ciò che permette a una singola firma di trasportare qualsiasi possibile fallimento.

L'overload Runnable.submitpool.submit(Runnable) — restituisce un Future<?> il cui get() restituisce null in caso di successo e incapsula comunque qualsiasi RuntimeException non catturata proveniente dal Runnable. È il modo standard per scoprire che un runnable "lancia e dimentica" è in realtà andato in crash.

I limiti di Future

Future è un canale unidirezionale: si invia, si attende, si ottiene il valore. Non si compone:

  • Non si può dire "quando questo finisce, esegui quello sul risultato."
  • Non si può dire "quando uno qualsiasi di questi N finisce, fai X."
  • Non si può dire "combina i risultati di questi due future senza bloccare."

Per tutto questo è necessario CompletableFuture (capitolo successivo). Future è lo strumento giusto quando:

  • Si vuole semplicemente un valore da un singolo task.
  • Si consuma un'API che restituisce Future e non serve composizione.
  • Il contratto più semplice è sufficiente.

Per il codice moderno che fa molta composizione asincrona, si salterà Future e si arriverà direttamente a CompletableFuture — ma Future è il tipo che l'executor service restituisce ancora da submit, quindi li si vedrà entrambi.

FutureTask — l'implementazione dietro submit

La classe che alimenta submit. Si può usare direttamente:

FutureTask<Integer> task = new FutureTask<>(() -> compute());
new Thread(task).start();                              // FutureTask is a Runnable
Integer v = task.get();

La maggior parte del codice non costruisce FutureTask direttamente; lo fa l'executor framework. È utile però quando si ha bisogno di un Future e di un Runnable in un unico oggetto — ad esempio per pianificarlo su qualcosa di diverso da un ExecutorService.

Esempio pratico: submit, timeout, propagazione

Il programma qui sotto invia un task lento, uno veloce e uno che fallisce; dimostra get, get(timeout), l'incapsulamento delle eccezioni e la cancellazione.

java— editable, runs on the server

Cosa osservare dall'esecuzione:

  • La sezione 1 è la forma più semplice: si invia un Callable, si chiama get, si riceve il valore. get ha bloccato il thread principale per i 50 ms impiegati dal task. È tutto ciò che Future fa nella sua forma base — un riferimento tipizzato e bloccante a un risultato che arriva in seguito.
  • La sezione 2 ha mostrato la forma con timeout. Il task lento avrebbe girato per 500 ms; get(100, MS) ha rinunciato dopo 100 ms e ha lanciato TimeoutException. Il successivo cancel(true) ha interrotto il thread in esecuzione affinché potesse uscire prima. Senza la cancellazione, il task avrebbe continuato a girare per i restanti 400 ms — occupando un thread del cui risultato non si aveva più bisogno.
  • La sezione 3 ha mostrato l'incapsulamento delle eccezioni. Il Callable ha lanciato IOException; get() l'ha rilanciata dentro ExecutionException. e.getCause() ha restituito l'originale. Questo è il canale universale di fallimento dell'API — qualsiasi throw, checked o unchecked, proveniente dal corpo del task arriva qui.
  • La sezione 4 ha mostrato la cancellazione di un task non ancora avviato. Con entrambi i thread del pool occupati su hog1 e hog2, il task queued era in attesa nella coda; cancel(false) lo ha rimosso senza mai eseguirlo. Chiamare get() sul future cancellato ha lanciato CancellationException — una modalità di fallimento diversa da "il task ha lanciato" (che sarebbe stata ExecutionException).
  • La sezione 5 ha mostrato invokeAny. Il task più veloce (50 ms) ha vinto; gli altri due sono stati cancellati dall'executor. invokeAny è lo strumento giusto per le query ridondanti — si chiamano più sorgenti, si usa il primo successo, si abbandonano le altre. È il mattone che sta alla base dei pattern di richieste hedged nei sistemi reali.

Cosa c'è dopo

Il capitolo successivo, Java CompletableFuture, introduce l'API asincrona componibile — thenApply, thenCompose, allOf, anyOf e i decine di combinatori che trasformano Future da un semplice riferimento a un risultato in una pipeline reattiva completa.

Esercizi

Pratica
Chiami `future.get()` e il task ha lanciato `SQLException` dal suo metodo `call()`. Quale eccezione lancia `get()`?
Chiami `future.get()` e il task ha lanciato `SQLException` dal suo metodo `call()`. Quale eccezione lancia `get()`?
Was this page helpful?