Как отменить выполнение CompletableFuture
Для отмены вычисления в CompletableFuture используется метод cancel(boolean mayInterruptIfRunning), унаследованный от интерфейса Future
🟢 Junior Level
Для отмены вычисления в CompletableFuture используется метод cancel(boolean mayInterruptIfRunning), унаследованный от интерфейса Future<T>.
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println("Прервано!");
}
return "Готово";
});
// Отменяем задачу:
boolean cancelled = future.cancel(true);
System.out.println("Отменен? " + future.isCancelled()); // true
System.out.println("Завершен? " + future.isDone()); // true
[!WARNING] Главная ловушка
CompletableFuture: вызовcancel(true)НЕ ПРЕРЫВАЕТ поток, исполняющий задачу! ПараметрmayInterruptIfRunningполностью игнорируется рантаймом.
При попытке получить результат отменённого фьючера через .get() или .join() немедленно выбрасывается java.util.concurrent.CancellationException.
🟡 Middle Level
Почему cancel() не прерывает поток
В классическом FutureTask (из Java 5) вызов cancel(true) запоминает поток-исполнитель и вызывает у него thread.interrupt().
В CompletableFuture дизайн принципиально иной:
CompletableFuture— это не задача (Runnable), а дескриптор состояния (Promise). Он может завершаться не потоком пула, а сетевым пакетом, таймером или сторонним событием.- Исходный код метода в JDK:
public boolean cancel(boolean mayInterruptIfRunning) { boolean cancelled = (result == null) && internalComplete(new AltResult(new CancellationException())); postComplete(); return cancelled || isCancelled(); }Параметр
mayInterruptIfRunningвообще не используется в реализации. Метод просто атомарно записываетAltResult(CancellationException)в полеresult. - Фоновый поток воркера продолжит крутить цикл или висеть на сокете до естественного завершения, тратя ресурсы CPU и памяти.
Как реализовать реальную отмену (Cooperative Cancellation)
Чтобы фоновая задача реально прекратила работу при отмене, необходимо использовать флаг совместной отмены (Cancellation Token):
public class CancellableWorker {
private final AtomicBoolean isCancelled = new AtomicBoolean(false);
public CompletableFuture<Void> doWorkAsync() {
return CompletableFuture.runAsync(() -> {
while (!isCancelled.get()) {
// Выполняем порцию работы
processBatch();
}
if (isCancelled.get()) {
System.out.println("Фоновый поток обнаружил отмену и мягко завершился.");
}
});
}
public void cancel() {
isCancelled.set(true);
}
}
🔴 Senior Level
Направление распространения отмены (Downstream vs Upstream)
В графе зависимостей CompletableFuture (DAG) отмена распространяется строго сверху вниз (Downstream) и никогда не идёт снизу вверх (Upstream).
CompletableFuture<String> source = CompletableFuture.supplyAsync(() -> fetchFromDb());
CompletableFuture<String> step1 = source.thenApply(s -> transform(s));
CompletableFuture<Void> step2 = step1.thenAccept(s -> save(s));
// Сценарий 1: Отмена родителя (Upstream)
source.cancel(true);
// Результат: source, step1 и step2 ВСЕ переходят в CancellationException.
// Сигнал отмены каскадно течет вниз по дереву зависимостей!
// Сценарий 2: Отмена дочернего узла (Downstream)
step2.cancel(true);
// Результат: только step2 переходит в CancellationException.
// Исходный source и step1 ПРОДОЛЖАЮТ РАБОТАТЬ и успешно завершаются!
[!IMPORTANT] Если клиент отменил HTTP-запрос на контроллере (
step2.cancel()), запрос в базу данных (source) не отменится, если вы не настроите обратную связь вручную!
Реакция whenComplete и handle на отмену
Отмена фьючера переводит его в статус завершённого с ошибкой (isCompletedExceptionally() == true).
Поэтому методы whenComplete и handle обязательно вызываются при отмене, получая CancellationException в качестве аргумента Throwable:
CompletableFuture<Resource> resourceFuture = acquireResourceAsync();
resourceFuture.whenComplete((resource, ex) -> {
if (ex instanceof CancellationException) {
log.warn("Задача была отменена! Освобождаем захваченные дескрипторы...");
}
closeSilently(resource);
});
// Отмена триггерит блок очистки в whenComplete:
resourceFuture.cancel(true);
4 Tricky Questions
1. Прерывает ли вызов future.cancel(true) поток, исполняющий задачу в CompletableFuture.supplyAsync?
Ответ:
Категорически нет.
В реализации CompletableFuture.cancel(boolean mayInterruptIfRunning) булев флаг mayInterruptIfRunning полностью игнорируется. Метод лишь переводит состояние самого объекта CompletableFuture в завершённое со значением AltResult(CancellationException). Сигнал Thread.currentThread().interrupt() на поток воркера не отправляется. Поток продолжит исполнять переданную лямбду до конца.
2. Что выбросит вызов .join() и вызов .get() на отменённом CompletableFuture? Оборачивается ли ошибка в ExecutionException?
Ответ:
Оба метода — и join(), и get() — при вызове на отменённом фьючере выбрасывают напрямую java.util.concurrent.CancellationException (непроверяемое исключение).
Оно НЕ оборачивается ни в ExecutionException (для get()), ни в CompletionException (для join()). Это специальное требование спецификации интерфейса Future: отмена всегда диагностируется отдельным типом исключения верхнего уровня.
3. Если цепочка состоит из 3 шагов: f1.thenApply(...).thenApply(...), и мы вызвали cancel(true) на последнем 3-м звене, прервутся ли вычисления на шагах 1 и 2?
Ответ:
Нет, шаги 1 и 2 не прервутся и не отменятся!
В CompletableFuture отмена не имеет механизма обратного распространения (Back-propagation / Upstream cancellation). Шаги 1 и 2 успешно завершатся в своих потоках, и только 3-й шаг перейдёт в состояние CancellationException. Если необходимо отменить всю цепочку, ссылку нужно сохранять на корневой источник (f1) и отменять именно его.
4. Может ли метод cf.complete(val) вернуть true, если фьючерс уже был отменён через cf.cancel(true)?
Ответ:
Нет, метод complete(val) вернёт false.
Метод cancel() переводит поле result в терминальное состояние AltResult(CancellationException) с помощью атомарной операции CAS. Любые последующие попытки завершить фьючерс через complete(val) или completeExceptionally(ex) увидят, что result != null, и вернут false, не оказав никакого влияния на состояние объекта.
🎯 Шпаргалка для интервью
Главное за 60 секунд
- Метод
cancel(boolean mayInterruptIfRunning)помечает фьючер как отменённый, но не прерывает исполняющий поток (параметр игнорируется). - При вызове
get()илиjoin()на отменённом фьючере напрямую выбрасываетсяCancellationException(без обёртки). - Отмена распространяется только вниз по цепочке (downstream): отмена корня отменяет всех детей, но отмена ребенка не отменяет родителей.
- Обработчики
whenCompleteиhandleвызываются при отмене и получаютCancellationException. - Для реальной остановки потока требуется Cooperative Cancellation (проверка внешнего флага
AtomicBooleanили флага прерывания).
Типичные ошибки на интервью
- Утверждать, что
cancel(true)прерывает поток черезThread.interrupt(). - Полагать, что отмена дочернего
CompletableFutureостанавливает выполнение родительских запросов. - Забывать, что
whenCompleteсработает даже в случае отмены задачи.