Что делает метод anyOf() и в каких случаях он полезен
Статический метод CompletableFuture.anyOf(CompletableFuture>... cfs) принимает переменное число асинхронных задач и возвращает новый CompletableFuture
🟢 Junior Level
Статический метод CompletableFuture.anyOf(CompletableFuture<?>... cfs) принимает переменное число асинхронных задач и возвращает новый CompletableFuture<Object>, который завершается в тот момент, когда любая первая из переданных задач завершает своё выполнение (как успешно, так и с ошибкой).
Время отклика метода равно времени самой быстрой операции: $\min(t_1, t_2, \dots, t_N)$.
Базовый пример: соревнование реплик (Fastest Wins)
CompletableFuture<String> primaryDc = CompletableFuture.supplyAsync(() -> queryDc("dc-east"));
CompletableFuture<String> replicaDc = CompletableFuture.supplyAsync(() -> queryDc("dc-west"));
// Ожидаем самый быстрый ответ из двух дата-центров:
CompletableFuture<Object> fastest = CompletableFuture.anyOf(primaryDc, replicaDc);
fastest.thenAccept(result -> {
// Внимание: результат имеет тип Object, требуется явный каст
String response = (String) result;
System.out.println("Первый полученный ответ: " + response);
});
Основные сценарии применения
- Hedged Requests (Запросы с упреждением): параллельная отправка одного и того же запроса нескольким независимым серверам для срезания “хвостов” задержки (Tail Latency).
- Параллельный опрос разнородных источников: одновременный запрос в быстрый кэш и базу данных, когда кэш обычно отвечает первым.
- Тайм-аут через соревнование: соревнование целевой задачи с фьючером задержки (в Java 8).
🟡 Middle Level
Две критические особенности anyOf
1. Потеря типобезопасности (CompletableFuture<Object>)
Так как в anyOf можно передать разнотипные фьючерсы (CompletableFuture<User>, CompletableFuture<Integer>, CompletableFuture<String>), метод возвращает CompletableFuture<Object>.
Для безопасного использования гомогенных (однотипных) задач создают типизированную обёртку:
@SuppressWarnings("unchecked")
public static <T> CompletableFuture<T> anyOfTyped(List<CompletableFuture<T>> futures) {
return (CompletableFuture<T>) CompletableFuture.anyOf(
futures.toArray(new CompletableFuture[0])
);
}
2. Проигравшие задачи НЕ отменяются автоматически
После победы самого быстрого фьючера все остальные продолжают работать в пуле потоков, удерживая процессор, соединения и память. Если их результат больше не нужен, разработчик обязан отменить их вручную:
List<CompletableFuture<Response>> futures = sendToAllReplicas();
CompletableFuture<Object> any = CompletableFuture.anyOf(futures.toArray(new CompletableFuture[0]));
any.thenAccept(winner -> {
// Отменяем всех проигравших для экономии ресурсов:
for (CompletableFuture<Response> f : futures) {
if (!f.isDone()) {
f.cancel(true);
}
}
});
Разница между anyOf и applyToEither
| Критерий | applyToEither / acceptEither |
CompletableFuture.anyOf |
|---|---|---|
| Количество участников | Ровно 2 фьючера | Произвольное количество ($N$) |
| Типобезопасность | Строго типизирован (T $\rightarrow$ V) |
Нетипизирован (Object) |
| Входные данные | Обязательно одинаковый тип T |
Произвольные гетерогенные типы |
🔴 Senior Level
Внутреннее устройство: бинарное дерево OrRelay
Аналогично методу allOf, anyOf строит сбалансированное бинарное дерево зависимостей, но вместо узлов BiRelay используются узлы OrRelay:
// Упрощенный фрагмент реализации в JDK:
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs) {
int n = cfs.length;
if (n == 0) return new CompletableFuture<Object>(); // Внимание: вечный pending!
if (n == 1) return (CompletableFuture<Object>) cfs[0].copy();
return orTree(cfs, 0, n - 1);
}
- Каждый узел
OrRelayслушает завершение левого или правого поддерева. - Тот поток, который завершает первый дочерний фьючер, с помощью атомарной операции CAS переводит корень
OrRelayв завершённое состояние. - Все последующие сигналы от других завершающихся фьючеров игнорируются CAS-механизмом.
Фатальная ловушка: anyOf завершается при ПЕРВОЙ ОШИБКЕ
Самое опасное заблуждение разработчиков — думать, что anyOf возвращает “первый успешный результат”.
Это не так! anyOf возвращает первое случившееся событие, независимо от того, успех это или авария:
- Если вы отправили 3 параллельных запроса к репликам, и первая ответившая реплика мгновенно вернула ошибку (например,
Connection refusedза 1 мс),anyOfнемедленно завершится с исключением! - Он не станет ждать, пока оставшиеся две реплики успешно вернут данные через 5 мс.
Паттерн “Первый успешный” (First Successful / Quorum)
Для реализации реального шаблона отказоустойчивости (Hedging) anyOf не подходит, и требуется писать координатор:
public static <T> CompletableFuture<T> firstSuccessful(List<CompletableFuture<T>> futures) {
CompletableFuture<T> promise = new CompletableFuture<>();
AtomicInteger remaining = new AtomicInteger(futures.size());
List<Throwable> errors = new CopyOnWriteArrayList<>();
for (CompletableFuture<T> f : futures) {
f.whenComplete((val, ex) -> {
if (ex == null) {
// Первый же успех выигрывает и отменяет остальных:
if (promise.complete(val)) {
futures.forEach(other -> other.cancel(true));
}
} else {
errors.add(ex);
// Если ВСЕ упали — завершаем ошибкой с накопленным списком сбоев:
if (remaining.decrementAndGet() == 0) {
CompletionException aggregate = new CompletionException("Все источники недоступны", errors.get(0));
errors.forEach(aggregate::addSuppressed);
promise.completeExceptionally(aggregate);
}
}
});
}
return promise;
}
4 Tricky Questions
1. Что вернёт CompletableFuture.anyOf() без аргументов (anyOf())?
Ответ:
Метод вернёт новый пустой CompletableFuture<Object>, который никогда не завершится (будет висеть бесконечно)!
В коде JDK проверка if (cfs.length == 0) return new CompletableFuture<Object>(); создаёт неинициализированный объект без подписчиков. Вызов join() на таком фьючере приведёт к вечной блокировке потока. Для сравнения: allOf() на пустом массиве возвращает уже завершённый completedFuture(null).
2. Если первая завершившаяся задача в anyOf упала с исключением, вернёт ли anyOf результат второй задачи, если та завершилась успехом через 1 миллисекунду?
Ответ:
Нет, не вернёт!
anyOf завершается сразу по первому факту наступления терминального состояния. Если первая завершившаяся задача упала с исключением, anyOf немедленно переходит в статус аварийного завершения (CompletionException). Успешный результат второй задачи будет просто проигнорирован.
3. Если переданные в anyOf задачи имеют разные типы (String и Integer), как избежать ClassCastException при чтении результата?
Ответ:
Так как anyOf стирает типы до Object, при гетерогенных источниках необходимо использовать сопоставление с образцом (Pattern Matching для switch / instanceof):
CompletableFuture.anyOf(fetchNameAsync(), fetchCountAsync())
.thenAccept(obj -> {
switch (obj) {
case String s -> processName(s);
case Integer i -> processCount(i);
case null -> log.warn("Результат null");
default -> throw new IllegalStateException("Неожиданный тип: " + obj.getClass());
}
});
4. В каком потоке выполнится колбэк, зарегистрированный через .thenAccept(...) на anyOf?
Ответ:
Колбэк выполнится в том потоке, который завершил ту самую задачу-победителя, которая пришла первой. Если к моменту регистрации колбэка какая-то задача уже была завершена, колбэк выполнится синхронно в вызывающем потоке. Для изоляции вычислений всегда рекомендуется указывать thenAcceptAsync(..., dedicatedExecutor).
🎯 Шпаргалка для интервью
Главное за 60 секунд
CompletableFuture.anyOf()ожидает завершения первого любого фьючера из переданных.- Возвращает
CompletableFuture<Object>. Задержка = $\min(t_1, t_2, \dots, t_N)$. - Реагирует на первое событие вообще: если самый быстрый упал —
anyOfсразу завершается ошибкой. - Не отменяет остальные задачи (их нужно отменять вручную).
- Вызов
anyOf()с пустым массивом возвращает “вечный” незавершающийся фьючерс. - Для ожидания “первого успешного” требуется собственная реализация (hedging pattern).
Типичные ошибки на интервью
- Полагать, что
anyOfждёт первый успешный ответ, игнорируя ошибки. - Забывать о продолжении работы проигравших задач в фоне.
- Путать
anyOf(произвольное число задач, возвращаетObject) иapplyToEither(2 задачи, строгая типизация).