Вопрос 11 · Раздел 19

В чём разница между thenApply() и thenApplyAsync()

Оба метода трансформируют результат CompletableFuture, но в разных потоках:

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Оба метода трансформируют результат CompletableFuture, но в разных потоках:

  • thenApply() — выполняется в том же потоке, который завершил предыдущий CF
  • thenApplyAsync() — выполняется в другом потоке (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);

Типичные ошибки

  1. 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]]