⏱️ Раздел 19 · Вопрос #10

Что делает метод 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);
});

Основные сценарии применения

  1. Hedged Requests (Запросы с упреждением): параллельная отправка одного и того же запроса нескольким независимым серверам для срезания “хвостов” задержки (Tail Latency).
  2. Параллельный опрос разнородных источников: одновременный запрос в быстрый кэш и базу данных, когда кэш обычно отвечает первым.
  3. Тайм-аут через соревнование: соревнование целевой задачи с фьючером задержки (в 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);
}
  1. Каждый узел OrRelay слушает завершение левого или правого поддерева.
  2. Тот поток, который завершает первый дочерний фьючер, с помощью атомарной операции CAS переводит корень OrRelay в завершённое состояние.
  3. Все последующие сигналы от других завершающихся фьючеров игнорируются 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 задачи, строгая типизация).

Связанные вопросы

☕ Java Interview Questions and Answers · Open Source Knowledge Portal

Built from the GitHub repository. Fully synced across English, Ukrainian, and Russian.