Чи можна повторно використовувати один CompletableFuture в декількох ланцюжках?
Зберігайте CompletableFuture в кеш замість самого значення:
🟢 Junior Level
Так, можна! Один CompletableFuture можна використовувати в декількох ланцюжках. Це називається fan-out — один результат, багато споживачів.
CompletableFuture<User> userCf = userService.fetchAsync(id);
// Три незалежні ланцюжки, один загальний CF
userCf.thenAccept(user -> loadOrders(user));
userCf.thenAccept(user -> checkBalance(user));
userCf.thenRun(() -> updateLastLogin());
Ключові моменти:
- Результат обчислюється рівно один раз
- Усі підписники отримують один і той самий об’єкт
- Якщо CF вже завершений — новий колбек виконається миттєво
🟡 Middle Level
Патерн: Promise Caching
Зберігайте CompletableFuture в кеш замість самого значення:
private final Map<Long, CompletableFuture<User>> cache = new ConcurrentHashMap<>();
public CompletableFuture<User> getUserAsync(Long id) {
return cache.computeIfAbsent(id, userId ->
userService.fetchAsync(userId)
.whenComplete((result, ex) -> {
// Видаляємо з кешу після завершення (успіх або помилка)
cache.remove(userId);
})
);
}
Перевага: Якщо 10 потоків запросять одні й ті самі дані одночасно, всі підпишуться на один CF. Тільки перший ініціює запит в БД, решта дочекаються результату.
Небезпека: мутація загального результату
Усі ланцюжки отримують посилання на один і той самий об’єкт.
CompletableFuture<User> userCf = userService.fetchAsync(id);
// Ланцюжок 1 — мутує об'єкт!
userCf.thenAccept(user -> user.setName("Modified"));
// Ланцюжок 2 — бачить змінені дані!
userCf.thenAccept(user -> System.out.println(user.getName())); // "Modified"
Правило: Результат, що передається в декілька ланцюжків, має бути імутабельним.
Порядок виконання колбеків
JDK не гарантує порядок виконання колбеків. У поточній реалізації (JDK 8-21)
порядок залежить від внутрішнього устрою. Якщо порядок критичний — явно задавайте
через thenCompose або thenCombine.
// ❌ Порядок не гарантований
cf.thenAccept(a -> step1(a));
cf.thenAccept(a -> step2(a)); // не обов'язково після step1
// ✅ Явний порядок
cf.thenCompose(a -> step1Async(a).thenAccept(r -> step2(r)));
🔴 Senior Level
Memory Leak механізм
Кожен залежний CF (результат thenApply, thenAccept і т.д.) зберігає посилання
на свій upstream. Поки залежна цепочка не завершена і на неї є посилання,
GC не може зібрати весь граф. У довгоживучих додатках це може призвести
до витоку, якщо створювати нескінченні ланцюжки без завершення.
// Проблема: нескінченне нарощування ланцюжків
CompletableFuture<Void> cf = CompletableFuture.completedFuture(null);
for (int i = 0; i < 1_000_000; i++) {
cf = cf.thenAccept(v -> doSomething());
// Кожен новий CF зберігає посилання на попередній
// Поки жив останній — GC не може зібрати весь граф
}
Cancellation propagation
Якщо скасувати кореневий CF, всі залежні ланцюжки завершаться з CancellationException. Відміна дочірньої цепочки не впливає на кореневий CF.
CompletableFuture<String> root = CompletableFuture.supplyAsync(() -> "data");
CompletableFuture<String> child1 = root.thenApply(s -> s + " -> 1");
CompletableFuture<String> child2 = root.thenApply(s -> s + " -> 2");
child1.cancel(false);
// root і child2 продовжують працювати
Архітектурний патерн: Request Collapsing
У highload-системах один CF використовується для запобігання дублюючим запитам:
public CompletableFuture<Data> getData(String key) {
return inFlight.computeIfAbsent(key, k ->
loadDataFromDb(k)
.whenComplete((v, ex) -> inFlight.remove(k))
);
}
// 100 одночасних запитів одного key → 1 реальний запит в БД
Best Practices
// ✅ Один CF → багато підписників (fan-out)
// ✅ Імутабельний результат
// ✅ Очищення кешу після завершення (whenComplete)
// ✅ Явний порядок через thenCompose/thenCombine
// ❌ Мутація загального результату
// ❌ Нескінченні ланцюжки без завершення
// ❌ Залежність від порядку колбеків
// ❌ Забуті посилання в кеші на завершені CF
🎯 Шпаргалка для співбесіди
Обов’язково знати:
- Один CF можна використовувати в декількох ланцюжках — fan-out патерн
- Результат обчислюється рівно один раз, усі підписники отримують один об’єкт
- Promise Caching: Map<Key, CompletableFuture> для запобігання дублюючим запитам
- Request Collapsing: 100 запитів одного key → 1 реальний запит в БД
- Порядок колбеків НЕ гарантується JDK
Часті уточнюючі питання:
- Порядок колбеків гарантований? — Ні. Якщо критичний — явно через thenCompose/thenCombine
- Memory leak як виникає? — Нескінченні ланцюжки: кожен CF зберігає посилання на upstream, GC не збирає
- Відміна дочірньої цепочки впливає на кореневий? — Ні. Відміна кореневого — впливає на всі дочірні
- Мутація загального результату — проблема? — Так, усі ланцюжки бачать один об’єкт. Результат має бути імутабельним
Червоні прапорці (НЕ говорити):
- «Кожен підписник отримує копію результату» — всі отримують один і той самий об’єкт
- «Порядок thenAccept гарантований» — JDK не гарантує порядок
- «Кеш на CF не потрібен — CF сам очиститься» — завершені CF потрібно видаляти з кешу
Пов’язані теми:
- [[16. Як правильно виконати декілька паралельних запитів до мікросервісів]]
- [[8. Як комбінувати результати декількох CompletableFuture]]
- [[17. Як скасувати виконання CompletableFuture]]
- [[23. Як тестувати код з CompletableFuture]]