Что делает метод orTimeout() в Java 9+
Метод orTimeout(long timeout, TimeUnit unit), появившийся в Java 9, устанавливает неблокирующий тайм-аут на выполнение CompletableFuture.
🟢 Junior Level
Метод orTimeout(long timeout, TimeUnit unit), появившийся в Java 9, устанавливает неблокирующий тайм-аут на выполнение CompletableFuture.
Если операция не успевает завершиться за отведённый интервал времени, CompletableFuture принудительно переводится в состояние ошибки с исключением java.util.concurrent.TimeoutException.
Базовый пример
CompletableFuture<String> paymentFuture = paymentClient.chargeAsync(orderId);
// Ограничиваем ожидание платежа 2 секундами:
paymentFuture.orTimeout(2, TimeUnit.SECONDS)
.thenAccept(receipt -> System.out.println("Платеж прошел: " + receipt))
.exceptionally(ex -> {
// При превышении 2 секунд управление попадает сюда:
// ex.getCause() содержит TimeoutException
System.err.println("Таймаут платежа: " + ex.getMessage());
return null;
});
Отличие от completeOnTimeout()
| Метод | Исход при таймауте | Типичный сценарий |
|---|---|---|
orTimeout(timeout, unit) |
Бросает TimeoutException |
Критическая операция (платеж, запись заказа) |
completeOnTimeout(fallback, timeout, unit) |
Успешно возвращает fallback |
Необязательные данные (кэш, блок рекомендаций) |
🟡 Middle Level
Важнейшая особенность: orTimeout() возвращает this
В отличие от методов трансформации (thenApply, thenCompose), которые порождают новый зависимый узел в дереве, метод orTimeout():
- Мутирует текущий экземпляр и возвращает ссылку на самого себя (
return this). - Регистрация таймера происходит непосредственно на том объекте, у которого вызван метод.
- Если на один и тот же
CompletableFutureвызватьorTimeout()несколько раз с разными значениями, победит самый короткий тайм-аут (так как первое срабатывание переведёт фьючерс в терминальное состояние через CAS).
Обработка исключения тайм-аута в цепочке
Ошибка TimeoutException автоматически оборачивается в CompletionException:
future.orTimeout(500, TimeUnit.MILLISECONDS)
.handle((res, ex) -> {
if (ex != null) {
Throwable root = (ex instanceof CompletionException) ? ex.getCause() : ex;
if (root instanceof TimeoutException) {
log.warn("Сервис ответил слишком медленно, активируем fallback");
return getFallbackData();
}
throw new CompletionException(ex);
}
return res;
});
🔴 Senior Level
Внутреннее устройство: Архитектура Delayer
Под капотом orTimeout() не создаёт отдельный поток операционной системы на каждый вызов:
- В JDK инициализируется приватный синглтон
Delayer:static final class Delayer { static final ScheduledThreadPoolExecutor delayer; static { (delayer = new ScheduledThreadPoolExecutor( 1, new DaemonThreadFactory())).setRemoveOnCancelPolicy(true); } } - Метод
orTimeout()ставит в очередьdelayerлегковесную задачу:delayer.schedule(new Timeout(this), timeout, unit); - При наступлении времени задача
Timeoutвыполняет:public void run() { if (f != null && !f.isDone()) f.completeExceptionally(new TimeoutException()); } - Устранение утечки памяти: Метод
orTimeoutсразу же регистрирует на фьючере слушательCancellerчерезwhenComplete. Если исходная задача завершается штатно за 10 мс при таймауте в 60 с,CancellerвызываетscheduledFuture.cancel(false). НастройкаsetRemoveOnCancelPolicy(true)гарантирует немедленное удаление отменённой задачи из очереди шедулера.
Смертельная ловушка: Блокировка единственного потока Delayer
Когда срабатывает тайм-аут, метод Timeout.run() выполняется внутри единственного на всю JVM системного потока Delayer.
Если после orTimeout зарегистрирован синхронный обработчик:
// ❌ КАТАСТРОФА ДЛЯ ВСЕЙ JVM:
future.orTimeout(1, TimeUnit.SECONDS)
.exceptionally(ex -> {
// Этот код выполняется ВНУТРИ ЕДИНСТВЕННОГО ПОТОКА Delayer!
Thread.sleep(5000); // БЛОКИРУЕТ Delayer на 5 секунд!
return fallback;
});
Задержка или синхронный I/O в этом обработчике заблокирует поток таймера всей виртуальной машины. В этот период ни один другой orTimeout() или completeOnTimeout() во всём приложении не сможет сработать вовремя!
Решение: Если логика обработки ошибки тяжёлая, используйте exceptionallyAsync(..., dedicatedExecutor) (Java 12+) или handleAsync(..., dedicatedExecutor).
4 Tricky Questions
1. В каком потоке выполнится лямбда внутри .exceptionally(...), если на этапе сработал orTimeout()?
Ответ:
Колбэк выполнится внутри единственного фонового потока шедулера CompletableFuture$Delayer!
Это критически важный нюанс: когда срабатывает таймер, задача Timeout.run() вызывается потоком Delayer и переводит фьючерс в статус ошибки через completeExceptionally(). Это немедленно и синхронно раскручивает стек подписчиков в том же потоке. Любые тяжелые вычисления или блокировки в таком exceptionally парализуют работу всех таймеров во всей JVM. Для изоляции следует использовать handleAsync(..., customExecutor).
2. Отменяет ли orTimeout() физическое выполнение фоновой задачи при срабатывании?
Ответ:
Нет, абсолютно не отменяет.
Метод orTimeout() лишь меняет состояние самого объекта CompletableFuture на TimeoutException. Поток пула, выполняющий тело задачи (например, долгий SQL-запрос или расчет), продолжит работать в фоне до своего естественного завершения. Чтобы остановить саму задачу, необходимо при возникновении TimeoutException явно отменять операцию через низкоуровневые интерфейсы (например, прерывать поток, закрывать сетевой сокет или отменять Statement.cancel()).
3. Что произойдёт, если вызвать cf.orTimeout(5, SECONDS) и затем на том же объекте cf.orTimeout(2, SECONDS)?
Ответ:
Сработает тайм-аут на 2 секунды.
Оба вызова зарегистрируют задачи в шедулере Delayer. Через 2 секунды первая задача переведёт фьючерс в состояние TimeoutException через CAS-операцию. Когда через 5 секунд сработает вторая задача, она обнаружит, что f.isDone() == true, и не окажет никакого эффекта.
4. Мутирует ли вызов orTimeout() исходный объект CompletableFuture или возвращает новый экземпляр?
Ответ:
Мутирует исходный объект и возвращает this.
В отличие от методов thenApply(), thenAccept() или thenCompose(), которые возвращают новые независимые экземпляры этапов, метод orTimeout() объявлен как:
public CompletableFuture<T> orTimeout(long timeout, TimeUnit unit) {
if (unit == null) throw new NullPointerException();
if (result == null)
whenComplete(new Canceller(Delayer.delay(new Timeout(this), timeout, unit)));
return this; // Возвращает ТОТ ЖЕ САМЫЙ объект!
}
Это означает, что тайм-аут навешивается непосредственно на текущий фьючерс, а не на его дочернюю копию.
🎯 Шпаргалка для интервью
Главное за 60 секунд
- Метод
orTimeout(timeout, unit)добавлен в Java 9. При истечении времени переводитCompletableFutureв состояниеTimeoutException. - Мутирует исходный фьючерс (
return this). - Использует один-единственный системный поток-демон
ScheduledThreadPoolExecutor(Delayer) на всю JVM. - Автоматически отменяет таймер при досрочном успешном завершении задачи (
setRemoveOnCancelPolicy(true)). - Смертельная ловушка: синхронный
exceptionallyпослеorTimeoutвыполняется в потокеDelayer. Блокировать его нельзя! - Таймаут не прерывает фоновый поток, исполняющий задачу в пуле.
Типичные ошибки на интервью
- Думать, что
orTimeout()доступен в Java 8 (метод появился только в Java 9). - Считать, что
orTimeout()создаёт новый зависимый фьючерс (он мутирует текущий). - Выполнять блокирующие операции в обработчиках ошибок после таймаута без суффикса
*Async.