⏱️ Розділ 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.