Что делает метод supplyAsync() и когда его использовать
Метод CompletableFuture.supplyAsync() — это главный фабричный метод для запуска асинхронных вычислений, возвращающих результат. Он принимает функциональный интерфейс Supplier
🟢 Junior Level
Метод CompletableFuture.supplyAsync() — это главный фабричный метод для запуска асинхронных вычислений, возвращающих результат. Он принимает функциональный интерфейс Supplier<T> (поставщик данных) и инициирует выполнение задачи в фоновом пуле потоков.
Базовый пример
// Запуск асинхронной задачи:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// Код выполняется в фоновом потоке
return "Данные из внешнего сервиса";
});
// Неблокирующее потребление результата:
future.thenAccept(result -> System.out.println("Получено: " + result));
Отличие supplyAsync() от runAsync()
| Метод | Принимаемый интерфейс | Возвращаемое значение | Типичное применение |
|---|---|---|---|
supplyAsync |
Supplier<T> (() -> T) |
CompletableFuture<T> |
Загрузка данных, вычисления, чтение |
runAsync |
Runnable (() -> void) |
CompletableFuture<Void> |
Отправка нотификаций, аудит, “fire-and-forget” |
// supplyAsync — возвращает значение T
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> fetchUser(42));
// runAsync — побочный эффект без результата
CompletableFuture<Void> auditFuture = CompletableFuture.runAsync(() -> sendAuditLog("User 42 logged in"));
🟡 Middle Level
Перегрузки метода и изоляция пулов
Метод существует в двух вариантах:
supplyAsync(Supplier<U> supplier): отправляет задачу в дефолтный системный пулForkJoinPool.commonPool().supplyAsync(Supplier<U> supplier, Executor executor): отправляет задачу в указанный кастомныйExecutor.
// Для I/O операций ВСЕГДА передавайте выделенный пул потоков:
ExecutorService ioExecutor = Executors.newFixedThreadPool(16);
CompletableFuture<List<Product>> productsFuture = CompletableFuture.supplyAsync(
() -> catalogClient.getProducts(),
ioExecutor
);
Проблема проверяемых исключений (Checked Exceptions)
Функциональный интерфейс Supplier<T> объявляет метод T get(), который не декларирует проверяемых исключений (throws Exception).
Если ваш метод выбрасывает, например, IOException или SQLException, компилятор выдаст ошибку:
// ❌ ОШИБКА КОМПИЛЯЦИИ: unhandled IOException
CompletableFuture.supplyAsync(() -> Files.readString(Path.of("config.json")));
// ✅ Решение 1: традиционный try-catch с обёртыванием в RuntimeException
CompletableFuture.supplyAsync(() -> {
try {
return Files.readString(Path.of("config.json"));
} catch (IOException e) {
throw new UncheckedIOException(e); // Обернётся в CompletionException
}
});
// ✅ Решение 2: SneakyThrows (Lombok) или универсальный ThrowingSupplier
CompletableFuture.supplyAsync(rethrow(() -> Files.readString(Path.of("config.json"))));
🔴 Senior Level
Внутреннее устройство: задача AsyncSupply
Под капотом вызов CompletableFuture.supplyAsync(supplier, executor) устроен следующим образом:
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor) {
return asyncSupplyStage(screenExecutor(executor), supplier);
}
static <U> CompletableFuture<U> asyncSupplyStage(Executor e, Supplier<U> f) {
if (f == null) throw new NullPointerException();
CompletableFuture<U> d = new CompletableFuture<U>();
e.execute(new AsyncSupply<U>(d, f)); // Заворачиваем в внутреннюю задачу
return d;
}
Класс AsyncSupply наследует ForkJoinTask<Void> и реализует Runnable и AsynchronousCompletionTask:
static final class AsyncSupply<T> extends ForkJoinTask<Void> implements Runnable, AsynchronousCompletionTask {
CompletableFuture<T> dep;
Supplier<? extends T> fn;
AsyncSupply(CompletableFuture<T> dep, Supplier<? extends T> fn) {
this.dep = dep; this.fn = fn;
}
public void run() {
CompletableFuture<T> d; Supplier<? extends T> f;
if ((d = dep) != null && (f = fn) != null) {
dep = null; fn = null; // Зануляем ссылки для сборщика мусора!
if (d.result == null) {
try {
d.completeValue(f.get()); // Успешная запись через CAS
} catch (Throwable ex) {
d.completeThrowable(ex); // Запись AltResult(ex)
}
}
d.postComplete(); // Пробуждение всех подписчиков в стеке
}
}
}
Ключевые архитектурные выводы:
- Ссылки на
depиfnзануляются сразу после старта выполнения, предотвращая утечки памяти (Memory Leaks), если лямбда захватила большой контекст. - Любой
Throwable(включаяErrorиRuntimeException) перехватывается и надёжно консервируется в полеresult, переводя фьючерс в аварийное состояние без краша треда пула.
4 Tricky Questions
1. Что произойдёт, если supplier.get() внутри supplyAsync вернёт null? Считается ли это ошибкой?
Ответ:
Нет, это не считается ошибкой.
Метод завершится успешно. Внутри CompletableFuture значение null распознаётся и упаковывается в специальный синглтон NIL (экземпляр AltResult с пустым полем исключения). При последующем вызове future.join() или передаче аргумента в thenAccept(val -> ...) значение NIL прозрачно распакуется обратно в обычный null.
2. В чём разница между executorService.submit(callable) и CompletableFuture.supplyAsync(supplier, executorService)?
Ответ:
executorService.submit(Callable<T>)возвращает классический блокирующийFuture<T>(Java 5). У него нет методов композиции, комбинирования и декларативной обработки ошибок без блокировкиget(). Кроме того,Callableможет объявлять проверяемые исключения (throws Exception).CompletableFuture.supplyAsync(Supplier<T>, executor)возвращаетCompletableFuture<T>с поддержкой контрактаCompletionStage(40+ методов неблокирующей композиции). ОднакоSupplierне позволяет бросать checked-исключения без обёртки.
3. Если переданная в supplyAsync() задача зависла, и мы вызвали future.cancel(true), прервётся ли выполнение задачи в потоке?
Ответ:
Нет, задача продолжит выполняться до конца!
В отличие от FutureTask, в CompletableFuture.cancel(boolean mayInterruptIfRunning) флаг mayInterruptIfRunning полностью игнорируется. Метод cancel() лишь атомарно записывает CancellationException в сам объект CompletableFuture. Сигнал Thread.interrupt() на исполняющий поток воркера не посылается. Если требуется прерываемость, необходимо передавать внешние флаги или использовать классический FutureTask.
4. Что произойдёт, если передать null в качестве supplier: CompletableFuture.supplyAsync(null)?
Ответ:
Метод немедленно выбросит NullPointerException в вызывающем потоке ещё до постановки задачи в очередь экзекутора.
Рантайм CompletableFuture выполняет предварительную строгую валидацию входных аргументов (if (f == null) throw new NullPointerException();), защищая конвейер от создания невалидных пустых задач.
🎯 Шпаргалка для интервью
Главное за 60 секунд
supplyAsync(Supplier<T>)— асинхронный запуск задачи с возвратом значенияT.runAsync(Runnable)— асинхронный запуск задачи без возврата значения (Void).- Без второго аргумента используется
ForkJoinPool.commonPool(). Для любых I/O операций обязательно передавать свойExecutor. Supplierне поддерживает Checked Exceptions — их нужно заворачивать вRuntimeException.- Возврат
nullиз лямбды абсолютно легален и завершает фьючерс успехом. future.cancel(true)не прерывает поток исполнения задачиsupplyAsync.
Типичные ошибки на интервью
- Путать
supplyAsync(возвращаетT) иrunAsync(возвращаетVoid). - Считать, что
cancel(true)посылаетThread.interrupt()на поток, исполняющийsupplyAsync. - Запускать блокирующий код через
supplyAsyncбез указания изолированного пула.