В чём разница между thenApply() и thenApplyAsync()
Оба метода трансформируют результат CompletableFuture, но в разных потоках:
🟢 Junior Level
Оба метода трансформируют результат CompletableFuture, но в разных потоках:
thenApply()— выполняется в том же потоке, который завершил предыдущий CFthenApplyAsync()— выполняется в другом потоке (ForkJoinPool или указанный Executor)
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
System.out.println("Supply: " + Thread.currentThread().getName());
return "Hello";
});
// thenApply — тот же поток
cf.thenApply(s -> {
System.out.println("thenApply: " + Thread.currentThread().getName());
return s + " World";
});
// thenApplyAsync — другой поток (ForkJoinPool)
cf.thenApplyAsync(s -> {
System.out.println("thenApplyAsync: " + Thread.currentThread().getName());
return s + " World";
});
🟡 Middle Level
Когда что использовать
thenApply — для лёгких операций:
// Лёгкая трансформация — нет смысла переключать поток
cf.thenApply(s -> s.toUpperCase())
.thenApply(s -> s.trim())
.thenAccept(System.out::println);
thenApplyAsync — для тяжёлых операций:
// Тяжёлая обработка — лучше в другом потоке
cf.thenApplyAsync(s -> heavyProcessing(s), executor)
.thenAccept(System.out::println);
С своим Executor:
ExecutorService executor = Executors.newFixedThreadPool(10);
cf.thenApplyAsync(s -> transform(s), executor);
Типичные ошибки
- thenApplyAsync без Executor: ```java // Использует ForkJoinPool.commonPool() cf.thenApplyAsync(s -> s.toUpperCase());
// ⚠️ commonPool имеет ограниченное число потоков // Для I/O операций — плохо
// ✅ Свой Executor cf.thenApplyAsync(s -> s.toUpperCase(), ioExecutor);
---
## 🔴 Senior Level
### Internal Implementation
**thenApply:**
```java
public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn) {
return uniApplyStage(null, fn);
}
// Выполняется в потоке, который завершил предыдущий CF
// Нет переключения потока — минимальный overhead
thenApplyAsync:
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn) {
return uniApplyStage(asyncPool, fn); // asyncPool = ForkJoinPool.commonPool()
}
public <U> CompletableFuture<U> thenApplyAsync(
Function<? super T,? extends U> fn, Executor executor
) {
return uniApplyStage(screenExecutor(executor), fn);
}
// asyncPool — поток из ForkJoinPool.commonPool()
// или указанный executor
Архитектурные Trade-offs
| thenApply | thenApplyAsync |
|---|---|
| Тот же поток | Другой поток |
| ~5 ns overhead | ~1μs overhead |
| Для лёгких операций | Для тяжёлых/блокирующих |
| Нет переключения контекста | Переключение потока |
Edge Cases
1. ThreadLocal:
// thenApply — ThreadLocal сохраняется
ThreadLocal<String> context = new ThreadLocal<>();
context.set("value");
cf.thenApply(s -> {
String ctx = context.get(); // OK — тот же поток
return s + ctx;
});
// thenApplyAsync — ThreadLocal потерян
cf.thenApplyAsync(s -> {
String ctx = context.get(); // null! — другой поток
return s;
});
2. Blocking operations:
// ❌ thenApply с блокирующей операцией
cf.thenApply(s -> {
Thread.sleep(1000); // блокирует поток ForkJoinPool!
return s;
});
// ✅ thenApplyAsync с отдельным Executor
cf.thenApplyAsync(s -> {
Thread.sleep(1000);
return s;
}, ioExecutor);
Производительность
thenApply:
- Overhead: ~5 ns
- No thread switch
thenApplyAsync (commonPool):
- Overhead: ~1μs
- Thread switch + queue
thenApplyAsync (custom executor):
- Overhead: ~2-5μs
- Thread switch + queue
Для лёгких операций: thenApply значительно быстрее thenApplyAsync (нет overhead на планирование задачи
в пул), но точное соотношение зависит от JVM, нагрузки и железа.
When NOT to use thenApplyAsync
- ThreadLocal контекст — Async может сменить поток, ThreadLocal потерян
- Тривиальная трансформация (getLength, toString) — overhead на scheduling > пользы
Production Experience
Pipeline:
// thenApply для лёгких трансформаций
fetchDataAsync()
.thenApply(data -> parseJson(data)) // лёгкая
.thenApply(obj -> validate(obj)) // лёгкая
.thenApplyAsync(valid -> transform(valid), transformExecutor) // тяжёлая
.thenAcceptAsync(result -> save(result), ioExecutor); // I/O
Best Practices
// ✅ thenApply для лёгких операций
cf.thenApply(s -> s.toUpperCase());
// ✅ thenApplyAsync для тяжёлых/блокирующих
cf.thenApplyAsync(s -> heavyProcessing(s), executor);
// ✅ Свой Executor для I/O
cf.thenApplyAsync(s -> ioOperation(s), ioExecutor);
// ❌ thenApplyAsync без причины (overhead)
// ❌ thenApply с блокирующими операциями
// ❌ commonPool для I/O
🎯 Шпаргалка для интервью
Обязательно знать:
- thenApply — в том же потоке что и предыдущий CF, ~5 ns overhead
- thenApplyAsync — в ForkJoinPool.commonPool() или указанном Executor, ~1μs overhead
- thenApply для лёгких CPU трансформаций, thenApplyAsync для тяжёлых/блокирующих
- ThreadLocal теряется при Async — другой поток
- Без Executor thenApplyAsync использует ForkJoinPool.commonPool()
Частые уточняющие вопросы:
- Когда thenApply, а когда thenApplyAsync? — thenApply для лёгких (toUpperCase), thenApplyAsync для тяжёлых (heavyProcessing, I/O)
- thenApplyAsync без Executor — какой пул? — ForkJoinPool.commonPool(), для I/O это thread pool starvation
- Почему thenApply быстрее? — Нет переключения потока, queue, scheduling overhead
- ThreadLocal с thenApplyAsync? — Потеряется, т.к. другой поток. Нужна явная передача контекста
Красные флаги (НЕ говорить):
- «thenApplyAsync всегда лучше — он асинхронный» — +~1μs overhead, для лёгких операций избыточен
- «thenApply блокирует поток» — он выполняется в потоке завершившего CF, без дополнительной блокировки
- «thenApplyAsync без Executor безопасен для I/O» — использует commonPool, thread pool starvation
Связанные темы:
- [[12. Какой пул потоков используется по умолчанию для async методов]]
- [[13. Как указать свой Executor для CompletableFuture]]
- [[4. В чём разница между thenApply() и thenCompose()]]
- [[15. Почему важно избегать блокирующих операций в CompletableFuture]]