1. Thread.interrupt() et annulation coopérative
Dans les applications réelles, les tâches peuvent être longues, et parfois même « se figer » — lors du travail avec le réseau, les fichiers ou des services externes. L’utilisateur peut annuler l’opération, le serveur — interrompre le traitement d’une requête, ou tout simplement la deadline générale expirer. Sans savoir annuler correctement les tâches, l’application va se bloquer, gaspiller des ressources et mal réagir aux événements externes.
Idée clé : l’annulation doit être coopérative — la tâche doit vérifier elle‑même si on lui a demandé de se terminer et libérer correctement les ressources.
Comment fonctionne Thread.interrupt()
Chaque thread possède un indicateur d’interruption. Lorsque vous appelez thread.interrupt(), ce drapeau est positionné à true. Le thread n’est pas pour autant « tué » : il doit vérifier son statut et se terminer lui‑même, par exemple en appelant périodiquement Thread.currentThread().isInterrupted() et en sortant proprement.
Exemple :
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// On travaille...
try {
Thread.sleep(100); // Peut être interrompu
} catch (InterruptedException e) {
// Le drapeau est effacé, mais nous pouvons nous réinterrompre
Thread.currentThread().interrupt();
break;
}
}
System.out.println("Thread terminé suite à l’interruption.");
});
worker.start();
// ... plus tard
worker.interrupt();
Où le drapeau agit‑il automatiquement ?
- Les méthodes susceptibles de se bloquer (sleep, wait, join, opérations des structures bloquantes) lèvent InterruptedException en cas d’interruption.
- Dans les autres cas (par exemple, dans une boucle de calcul), il faut vérifier manuellement isInterrupted().
Patron « poser le drapeau — et sortir rapidement »
- Dans le code appelant : thread.interrupt()
- Dans la tâche : vérifier périodiquement Thread.currentThread().isInterrupted()
- Si nécessaire — libérer correctement les ressources et se terminer.
Erreur typique : s’attendre à ce que interrupt() « tue » le thread instantanément. Non — ce n’est qu’un signal, la tâche doit y réagir elle‑même.
2. Future.cancel(), CancellationException et annulation des tâches
Comment fonctionne Future.cancel
Quand vous lancez une tâche via ExecutorService.submit(), vous obtenez un objet Future. Il possède la méthode cancel(boolean mayInterruptIfRunning) :
- Si la tâche n’a pas encore commencé — elle ne sera pas lancée.
- Si la tâche est déjà en cours et que mayInterruptIfRunning == true — interrupt() sera appelé sur le thread qui exécute la tâche.
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<?> future = executor.submit(() -> {
while (!Thread.currentThread().isInterrupted()) {
// Travail long
}
System.out.println("Tâche terminée suite à l’annulation.");
});
// ... plus tard
future.cancel(true); // Demandons l’annulation de la tâche
Ce qui arrive réellement à la tâche
L’annulation via Future n’est pas un bouton magique « tuer le thread », mais en pratique une forme polie de Thread.interrupt(). Si la tâche vérifie correctement le drapeau d’interruption — elle se terminera proprement. Sinon — elle continuera jusqu’à sa fin naturelle.
Si vous appelez future.get() après l’annulation, vous obtiendrez une CancellationException — un rappel que la tâche a été annulée.
3. CompletableFuture : annulation, délais d’expiration et chaînages
Annulation de CompletableFuture
CompletableFuture possède aussi cancel(boolean). Si la tâche n’est pas encore terminée, elle sera annulée, et tous les gestionnaires suivants (thenApply, thenAccept, etc.) ne seront pas appelés.
CompletableFuture<Void> cf = CompletableFuture.runAsync(() -> {
while (!Thread.currentThread().isInterrupted()) {
// On travaille...
}
System.out.println("CF terminé suite à l’annulation.");
});
// ... plus tard
cf.cancel(true);
Délais d’expiration : orTimeout et completeOnTimeout
- orTimeout(timeout, unit) — termine CompletableFuture avec une TimeoutException si elle n’a pas fini à temps.
- completeOnTimeout(value, timeout, unit) — termine avec la valeur indiquée si elle n’a pas fini à temps.
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(5000); } catch (InterruptedException e) {}
return "OK";
});
cf.orTimeout(2, TimeUnit.SECONDS)
.exceptionally(ex -> "TIMEOUT")
.thenAccept(System.out::println); // Après 2 secondes : "TIMEOUT"
Propagation de l’annulation dans les chaînes
Si vous annulez le CompletableFuture « supérieur », toutes les étapes suivantes de la chaîne ne seront pas appelées. Mais lors de l’utilisation de thenCompose pour lancer des opérations asynchrones internes, l’annulation ne se propage pas « vers le haut » automatiquement — il faut la concevoir explicitement (vérifier le statut, annuler les tâches filles, utiliser une deadline commune).
Attention avec thenCompose et un Executor personnalisé ! Assurez‑vous que les tâches internes savent réagir à l’interruption/annulation et/ou reçoivent une deadline commune.
4. StructuredTaskScope : annulation d’un groupe de tâches
Structured Concurrency et annulation
StructuredTaskScope (Java 21+) permet de lancer un groupe de tâches et de gérer leur cycle de vie comme un tout. Si l’une des tâches se termine avec une erreur ou si le timeout expire — les autres tâches sont automatiquement annulées.
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> f1 = scope.fork(() -> fetchData1());
Future<String> f2 = scope.fork(() -> fetchData2());
scope.join(); // on attend la fin de toutes les tâches
scope.throwIfFailed(); // si au moins une a échoué — on lève une exception
String result = f1.resultNow() + f2.resultNow();
System.out.println(result);
}
- Si une tâche se termine avec une erreur — le scope annule toutes les autres tâches.
- Si le timeout expire (via scope.joinUntil(deadline)) — le scope annule toutes les tâches.
Politiques de terminaison
- ShutdownOnFailure — annule toutes les tâches à la première erreur.
- ShutdownOnSuccess — annule les autres tâches dès qu’une s’est terminée avec succès.
5. Pratique : annuler en toute sécurité des opérations longues
Exemple : annulation d’E/S bloquantes
Si une tâche est bloquée en lecture de fichier ou de réseau, l’interruption du thread n’aide pas toujours — certaines opérations d’E/S ne réagissent pas à interrupt. Dans les API modernes (NIO, AsynchronousFileChannel), l’interruption est mieux prise en charge, mais pas partout.
Recommandations :
- Utilisez des E/S non bloquantes si vous avez besoin d’annulation.
- Pour des E/S bloquantes — définissez des délais d’expiration au niveau de l’API (par exemple, Socket.setSoTimeout).
- Pour les tâches asynchrones — utilisez Future.cancel et réagissez correctement à l’interruption.
Exemple : annulation de l’attente d’une file/barrière
Beaucoup de synchroniseurs (BlockingQueue.take(), CountDownLatch.await(), CyclicBarrier.await()) lèvent InterruptedException en cas d’interruption. Dans le gestionnaire, interceptez l’exception, rétablissez le drapeau si nécessaire et terminez correctement la tâche.
6. Modèle « time‑budget » : deadline commune pour un groupe d’opérations
Dans les applications complexes, il est souvent nécessaire de définir une deadline commune pour l’exécution d’un groupe d’opérations. Par exemple, si l’utilisateur attend une réponse pas plus de 2 secondes, et qu’il faut effectuer 3 requêtes réseau — elles doivent toutes tenir dans la deadline commune.
Comment propager la deadline vers le bas de la pile ?
- Transmettez un objet de deadline (par exemple, Instant deadline) à toutes les méthodes potentiellement bloquantes.
- Dans chaque méthode, calculez le temps restant : Duration.between(Instant.now(), deadline).
- Utilisez ce temps pour les délais d’expiration des opérations bloquantes (await(timeout), poll(timeout), orTimeout(timeout)).
Instant deadline = Instant.now().plusSeconds(2);
void doWork(Instant deadline) throws TimeoutException, InterruptedException {
Duration left = Duration.between(Instant.now(), deadline);
if (left.isNegative() || left.isZero()) throw new TimeoutException();
// On utilise left pour le timeout
queue.poll(left.toMillis(), TimeUnit.MILLISECONDS);
}
Scoped Values / contexte
En Java 21+, on peut utiliser les Scoped Values pour transmettre la deadline le long de la pile d’appels, sans la passer explicitement à chaque méthode.
7. Structured Concurrency : annuler tout le scope en cas d’échec/timeout
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> f1 = scope.fork(() -> fetchData1());
Future<String> f2 = scope.fork(() -> fetchData2());
boolean completed = scope.joinUntil(Instant.now().plusSeconds(2));
if (!completed) {
scope.shutdown();
throw new TimeoutException("Deadline dépassée !");
}
scope.throwIfFailed();
// ...
}
- Si la deadline est dépassée — le scope annule toutes les tâches.
- Si une tâche échoue — les autres sont annulées automatiquement.
8. Erreurs courantes lors de l’annulation et des délais d’expiration
Erreur n° 1 : s’attendre à ce que interrupt() termine immédiatement le thread. En réalité, ce n’est qu’un signal — la tâche doit vérifier son statut et se terminer correctement.
Erreur n° 2 : ne pas vérifier isInterrupted() dans les longues boucles. Si le drapeau d’interruption n’est pas vérifié, la tâche continuera indéfiniment, même si on lui a demandé de se terminer.
Erreur n° 3 : Future.cancel() ne conduit pas à une annulation si la tâche n’écoute pas l’interruption. Si la tâche est « sourde », cancel() ne vous aidera pas.
Erreur n° 4 : les délais d’expiration ne sont pas propagés vers le bas de la pile. Si vous ne transmettez pas la deadline à toutes les méthodes, une opération interne peut « se bloquer » plus longtemps que nécessaire.
Erreur n° 5 : dans les chaînes thenCompose de CompletableFuture l’annulation ne se propage pas automatiquement. Si vous annulez le Future « supérieur », des tâches internes peuvent continuer à s’exécuter — gérez explicitement l’annulation.
Erreur n° 6 : StructuredTaskScope n’est pas fermé (pas de try‑with‑resources). Si le scope n’est pas fermé, des tâches filles peuvent rester « pendantes ».
GO TO FULL VERSION