Что такое асинхронное программирование в Java и какие основные паттерны существуют?
Асинхронное программирование — это подход, при котором задачи, не требующие немедленного результата (сетевые запросы, обращения к БД, файловый ввод-вывод), выполняются без блокировки вызывающего потока. Вместо ожидания завершения поток может выполнять другую работу.
Основные паттерны:
1. Callback (обратный вызов) — самый ранний подход
Код: паттерн Callback
interface AsyncCallback<T> {
void onSuccess(T result);
void onFailure(Exception ex);
}
void fetchDataAsync(String url, AsyncCallback<String> callback) {
new Thread(() -> {
try {
String result = doHttpRequest(url);
callback.onSuccess(result);
} catch (Exception e) {
callback.onFailure(e);
}
}).start();
}
// Использование
fetchDataAsync("https://api.example.com", new AsyncCallback<>() {
@Override
public void onSuccess(String result) { System.out.println(result); }
@Override
public void onFailure(Exception ex) { ex.printStackTrace(); }
});
Проблема: при вложении нескольких асинхронных вызовов возникает «callback hell» — код становится нечитаемым.
2. Future/Promise — объект-представление будущего результата
Пример
// Future (Java 5) — только блокирующее получение
Future<String> future = executor.submit(() -> fetchData());
String result = future.get(); // блокирует текущий поток
// CompletableFuture (Java 8) — неблокирующие цепочки
CompletableFuture.supplyAsync(() -> fetchData())
.thenApply(data -> parse(data))
.thenAccept(parsed -> save(parsed))
.exceptionally(ex -> { log.error(ex); return null; });
3. Реактивные потоки (Reactive Streams) — Publisher-Subscriber с backpressure
Пример
Flux.fromIterable(userIds)
.flatMap(id -> webClient.get().uri("/users/" + id)
.retrieve().bodyToMono(User.class))
.filter(User::isActive)
.collectList()
.subscribe(users -> processUsers(users));
Реализации: Project Reactor, RxJava, Java 9 Flow API.
4. Async/Await через Virtual Threads (Java 21)
Синхронный по виду код, который работает асинхронно за счёт дешёвого переключения виртуальных потоков:
Пример
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<String> user = executor.submit(() -> fetchUser(id));
Future<String> orders = executor.submit(() -> fetchOrders(id));
System.out.println(user.get() + " : " + orders.get());
}
5. Event Loop (цикл событий) — один поток обрабатывает события из очереди
Пример
Vertx vertx = Vertx.vertx();
vertx.createHttpServer()
.requestHandler(req -> req.response().end("Hello"))
.listen(8080);
Используется в Vert.x, Netty.
Выбор паттерна:
| Задача | Рекомендуемый паттерн |
|---|---|
| Единичные асинхронные вызовы | CompletableFuture |
| Потоки данных с backpressure | Reactive Streams |
| Высоконагруженные серверы | Event Loop или Virtual Threads |
| Простые IO-bound задачи (Java 21+) | Virtual Threads |
| Оркестрация нескольких параллельных задач | CompletableFuture или Structured Concurrency |
Ключевые ошибки:
- Смешивание блокирующего и асинхронного кода (
future.get()внутри реактивной цепочки). - Отсутствие обработки ошибок в асинхронных цепочках — исключения теряются.
- Чрезмерное усложнение: реактивные потоки там, где достаточно Virtual Threads.
- Отсутствие таймаутов — утечка ресурсов.
Аналогия: представьте ресторан. Callback — это когда официант стоит у кухни и ждёт ваше блюдо. Future — вы взяли номерок и сидите за столом. Reactive — конвейер суши, где блюда едут к вам автоматически с контролем скорости. Virtual Threads — у вас миллион официантов, каждый обслуживает одного клиента, но стоить это как десять.
На собеседовании важно показать понимание: асинхронность != параллелизм. Асинхронность может быть реализована в одном потоке (Event Loop). Тренд 2024-2026 — переход от реактивных фреймворков к Virtual Threads для IO-bound задач.