У чому різниця між 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]]