Можно ли повторно использовать один 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]]