Що робить метод 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 завдання, сувора типізація).