W3docs

Java Callable et Future

Retournez des valeurs depuis des tâches avec Callable et consommez-les de manière asynchrone avec Future — attente, timeout, annulation, exceptions.

Runnable permet à un thread d'exécuter du travail, mais ne permet pas de retourner une valeur ni de lever une exception vérifiée. La paire qui le permet est Callable<V> (le producteur) et Future<V> (le consommateur). Vous soumettez un Callable<V> à un ExecutorService et récupérez un Future<V>, qui est votre handle pour : attendre le résultat, lire la valeur, capturer l'exception de la tâche ou l'annuler.

Il s'agit de l'API de plus bas niveau sensible aux résultats dans la boîte à outils concurrente de Java. Le prochain chapitre, CompletableFuture, ajoute des chaînes, des combinateurs et des pipelines ; mais le contrat — « un résultat asynchrone sur lequel on peut attendre » — est ce que Future a défini en premier, et c'est encore le bon outil pour un simple « va faire ça et dis-moi quand c'est terminé ».

Callable<V> — Runnable avec un type de retour

L'interface :

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

Les deux différences par rapport à Runnable :

  1. Retourne V (le paramètre de type).
  2. Peut lever n'importe quelle Exception — y compris les exceptions vérifiées.

Comme Runnable, c'est une interface fonctionnelle — les lambdas et les références de méthodes fonctionnent :

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 est la bonne forme pour tout travail du type « va faire ça et rapporte-moi une valeur ». Runnable ne convient que si vous n'avez vraiment pas besoin d'un résultat.

Future<V> — le handle vers un résultat asynchrone

Quand vous submittez un Callable<V>, l'executor retourne 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;
}

Cinq méthodes. Trois que vous utiliserez souvent.

get()

Bloque le thread appelant jusqu'à ce que la tâche se termine, puis retourne le résultat :

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() lève trois choses que vous devez gérer :

  • InterruptedException — l'appelant a été interrompu pendant l'attente. Traitement standard : réinitialiser le drapeau d'interruption et propager.
  • ExecutionException — la tâche elle-même a levé une exception. L'exception d'origine est encapsulée ; accédez-y via .getCause().
  • CancellationException — quelqu'un a appelé cancel() sur le future.

Une forme courante :

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)

Identique à get() mais avec une échéance. Lève TimeoutException si la tâche ne se termine pas à temps :

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

C'est la bonne forme pour « j'appelle un backend qui devrait répondre en N ms ; sinon, échouer rapidement ». Associez toujours le catch à un cancel(true) — sinon la tâche continue de s'exécuter en arrière-plan, utilisant un thread dont vous ne vous souciez plus du résultat.

cancel(boolean)

Demande à la tâche de s'arrêter :

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

L'argument indique à l'executor s'il doit interrompre le thread de travail. Avec true, le worker reçoit une InterruptedException depuis tout appel bloquant (sleep, wait, I/O) ; avec false, l'annulation est sans effet si la tâche a déjà démarré — seules les tâches non démarrées sont retirées de la file d'attente.

cancel est coopératif. Une tâche qui ne vérifie pas Thread.currentThread().isInterrupted() et n'a pas d'appels bloquants continuera à s'exécuter jusqu'à ce qu'elle se termine. L'annulation n'est pas un interrupteur d'arrêt — c'est une requête que la tâche doit honorer.

Exceptions : la règle d'encapsulation

Tout ce que le Callable lève est encapsulé dans ExecutionException lorsque vous appelez get. La cause est le throwable d'origine :

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

Notez l'asymétrie : le Callable peut lever une exception vérifiée (la clause throws Exception dans sa signature), mais Future.get ne déclare que ExecutionException. L'encapsulation est ce qui permet à une seule signature de transporter chaque échec possible.

La surcharge Runnable.submitpool.submit(Runnable) — retourne un Future<?> dont get() retourne null en cas de succès et encapsule tout RuntimeException non capturé du Runnable. C'est la façon standard de découvrir qu'un runnable « fire and forget » a en fait planté.

Les limites de Future

Future est un canal unidirectionnel : vous soumettez, vous attendez, vous obtenez la valeur. Il ne se compose pas :

  • Vous ne pouvez pas dire « quand ça se termine, exécute ça sur le résultat ».
  • Vous ne pouvez pas dire « quand l'un de ces N se termine, fais X ».
  • Vous ne pouvez pas dire « combine les résultats de ces deux futures sans bloquer ».

Pour tout cela, vous avez besoin de CompletableFuture (prochain chapitre). Future est le bon outil quand :

  • Vous voulez simplement une valeur de retour depuis une seule tâche.
  • Vous consommez une API qui retourne des Futures et n'avez pas besoin de les composer.
  • Le contrat le plus simple suffit.

Pour du code moderne qui fait beaucoup de composition asynchrone, vous passerez généralement directement à CompletableFuture — mais Future est le type que l'executor service retourne toujours depuis submit, donc vous verrez les deux.

FutureTask — l'implémentation derrière submit

La classe qui propulse submit. Vous pouvez l'utiliser directement :

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

La plupart du code ne construit pas FutureTask directement ; le framework executor s'en charge. Mais c'est utile quand vous avez besoin d'un Future et d'un Runnable dans un seul objet — par exemple pour le planifier sur autre chose qu'un ExecutorService.

Un exemple concret : soumettre, timeout, propagation

Le programme ci-dessous soumet une tâche lente, une tâche rapide et une tâche qui échoue ; il démontre get, get(timeout), le dépaquetage des exceptions et l'annulation.

java— editable, runs on the server

Ce qu'il faut retenir de l'exécution :

  • La section 1 est la forme la plus simple : soumettre un Callable, appeler get, recevoir la valeur. get a bloqué le thread principal pendant les 50 ms que la tâche a pris. C'est tout ce que Future fait dans sa forme basique — un handle typé et bloquant vers un résultat qui arrive plus tard.
  • La section 2 a montré la forme avec timeout. La tâche lente aurait tourné 500 ms ; get(100, MS) a abandonné après 100 ms et a levé TimeoutException. Le cancel(true) suivant a interrompu le thread en cours d'exécution pour qu'il puisse terminer plus tôt. Sans l'annulation, la tâche aurait continué à tourner pendant les 400 ms restantes — utilisant un thread dont vous ne vous souciez plus du résultat.
  • La section 3 a montré l'encapsulation des exceptions. Le Callable a levé IOException ; get() l'a re-levée dans ExecutionException. e.getCause() a redonné l'original. C'est le canal universel d'échec de l'API — chaque throw vérifié ou non vérifié du corps atterrit ici.
  • La section 4 a montré l'annulation d'une tâche non démarrée. Les deux threads du pool étant occupés sur hog1 et hog2, la tâche queued était dans la file de travail ; cancel(false) l'a retirée sans jamais l'exécuter. Appeler get() sur le future annulé a levé CancellationException — un mode d'échec différent de « la tâche a levé » (qui aurait été ExecutionException).
  • La section 5 a montré invokeAny. La tâche la plus rapide (50 ms) a gagné ; les deux autres ont été annulées par l'executor. invokeAny est le bon outil pour les requêtes redondantes — appeler plusieurs sources, utiliser le premier succès, abandonner le reste. C'est le bloc de base derrière les patterns de requêtes hedgées dans les systèmes réels.

Et ensuite

Le prochain chapitre, Java CompletableFuture, présente l'API asynchrone composable — thenApply, thenCompose, allOf, anyOf, et les dizaines de combinateurs qui transforment Future d'un simple handle à un résultat en un pipeline réactif complet.

Pratique

Pratique
Vous appelez `future.get()` et la tâche a levé `SQLException` depuis sa méthode `call()`. Quelle exception `get()` lève-t-il ?
Vous appelez `future.get()` et la tâche a levé `SQLException` depuis sa méthode `call()`. Quelle exception `get()` lève-t-il ?
Was this page helpful?