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

Что делает метод supplyAsync() и когда его использовать

Structured Java interview answer with junior, middle, and senior-level explanation.

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

🟢 Junior Level

supplyAsync() — создаёт CompletableFuture, который выполняет задачу асинхронно и возвращает результат.

// supplyAsync — задача с возвратом значения
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
    // Выполняется в другом потоке
    return "Hello from async";
});

// Получить результат
cf.thenAccept(result -> System.out.println(result));

Отличие от runAsync():

  • supplyAsync() — возвращает значение (Supplier<T>)
  • runAsync() — не возвращает (Runnable)
// supplyAsync — с результатом
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> "result");

// runAsync — без результата
CompletableFuture<Void> cf2 = CompletableFuture.runAsync(() -> {
    System.out.println("Done");
});

🟡 Middle Level

Когда использовать

supplyAsync:

// Когда нужен результат
CompletableFuture<User> user = CompletableFuture.supplyAsync(() -> {
    return userRepository.findById(userId);
});

CompletableFuture<String> data = CompletableFuture.supplyAsync(() -> {
    return httpClient.get(url);
});

С своим Executor:

ExecutorService ioExecutor = Executors.newFixedThreadPool(10);

CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
    return httpClient.get(url);
}, ioExecutor);

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

  1. Blocking в commonPool: ```java // ❌ Блокирует ForkJoinPool CompletableFuture.supplyAsync(() -> { return httpClient.get(url); // blocking I/O });

// ✅ Свой Executor CompletableFuture.supplyAsync(() -> httpClient.get(url), ioExecutor);


### Когда НЕ использовать supplyAsync

- **Простые синхронные операции** — overhead на создание CF не оправдан
- **Жёсткий порядок выполнения** — используйте последовательные вызовы
- **Нужна отмена с прерыванием** — лучше FutureTask

---

## 🔴 Senior Level

### Internal Implementation

```java
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
    return asyncSupplyStage(asyncPool, supplier);
}

public static <U> CompletableFuture<U> supplyAsync(
    Supplier<U> supplier, Executor executor
) {
    return asyncSupplyStage(screenExecutor(executor), supplier);
}

// asyncPool = ForkJoinPool.commonPool()
// Создаёт CompletableFuture и планирует задачу в executor
// Упрощённая псевдокод-репрезентация. Реальные имена методов в JDK отличаются.

Архитектурные Trade-offs

supplyAsync runAsync
Возвращает T Возвращает Void
Supplier Runnable
Для вычислений Для side effects

Edge Cases

1. Exception in supplier:

CompletableFuture.supplyAsync(() -> {
    throw new RuntimeException("Error");
}).exceptionally(ex -> "recovered");

2. Long-running tasks:

// ❌ Долгая задача в commonPool
CompletableFuture.supplyAsync(() -> {
    Thread.sleep(60000);  // блокирует 60 секунд
    return result;
});

// ✅ Свой Executor
CompletableFuture.supplyAsync(longTask, longTaskExecutor);

Performance

supplyAsync():
- Создание: ~10 ns
- Планирование: ~1μs
- Thread switch: ~1-5μs

commonPool overhead: минимальный
Custom executor: + queue overhead

Production Experience

@Service
public class OrderService {
    private final ExecutorService ioExecutor;
    
    public CompletableFuture<Order> getOrderAsync(Long id) {
        return CompletableFuture.supplyAsync(() -> {
            return orderRepository.findById(id);
        }, ioExecutor);
    }
    
    // Комбинирование
    public CompletableFuture<OrderSummary> getSummaryAsync(Long id) {
        CompletableFuture<Order> order = getOrderAsync(id);
        CompletableFuture<User> user = getUserAsync(id);
        
        return order.thenCombine(user, (o, u) -> 
            new OrderSummary(o, u)
        );
    }
}

Best Practices

// ✅ supplyAsync для вычислений
CompletableFuture.supplyAsync(() -> calculate());

// ✅ Свой Executor для I/O
CompletableFuture.supplyAsync(ioTask, ioExecutor);

// ✅ Virtual Threads (Java 21+)
CompletableFuture.supplyAsync(task, vThreads);

// ❌ Blocking в commonPool
// ❌ Долгие задачи без своего Executor

🎯 Шпаргалка для интервью

Обязательно знать:

  • supplyAsync(Supplier) — асинхронная задача с возвратом результата
  • runAsync(Runnable) — асинхронная задача без возврата (Void)
  • Без Executor: ForkJoinPool.commonPool(), с Executor: указанный пул
  • Для I/O всегда передавайте свой Executor
  • supplyAsync для вычислений и fetch-операций, runAsync для side effects

Частые уточняющие вопросы:

  • supplyAsync vs runAsync — когда что? — supplyAsync когда нужен результат (Supplier), runAsync когда нет (Runnable)
  • supplyAsync без Executor — какой пул? — ForkJoinPool.commonPool(), для I/O это thread pool starvation
  • Exception в supplier — что будет? — CF завершится с CompletionException, нужно exceptionally/handle
  • supplyAsync vs thenApplyAsync? — supplyAsync создаёт новый CF, thenApplyAsync трансформирует существующий

Красные флаги (НЕ говорить):

  • «supplyAsync и runAsync это одно и то же» — supplyAsync возвращает T, runAsync возвращает Void
  • «supplyAsync без Executor безопасен для HTTP» — blocking I/O в commonPool → starvation
  • «supplyAsync создаёт новый поток» — использует ForkJoinPool.commonPool() или указанный Executor

Связанные темы:

  • [[12. Какой пул потоков используется по умолчанию для async методов]]
  • [[13. Как указать свой Executor для CompletableFuture]]
  • [[5. Что делают методы thenAccept() и thenRun()]]
  • [[15. Почему важно избегать блокирующих операций в CompletableFuture]]