Перейти к основному содержимому

Асинхронность в Java

Разработчику Архитектору

Асинхронность в Java

Асинхронность — «запустил задачу и не жду у стены, пока она закончится». В Java для этого есть несколько уровней — от тяжёлых потоков ОС до лёгких virtual threads (Java 21+).

Эта статья — практический выбор инструмента, не дублирование JVM и потоков (там память, synchronized, GC).

Ваша задачаС чего начать
Параллельно посчитать отчётExecutorService с ограниченным пулом
Несколько HTTP/БД вызовов в одном запросеCompletableFuture или virtual threads
Миллионы одновременных блокирующих I/OVirtual threads (Java 21+)
Весь стек reactive end-to-endWebFlux — Spring

На Kotlin те же идеи проще выражаются — корутины.


Thread и Runnable

Поток ОС — тяжёлый ресурс (стек, планировщик). Создание «на каждый запрос» не масштабируется.

Thread t = new Thread(() -> {
System.out.println(Thread.currentThread().getName());
});
t.start();
t.join();

Интерфейс Runnable (или Callable<V> с результатом) описывает задачу; поток её выполняет.


ExecutorService

Пул переиспользует ограниченное число потоков для множества задач:

ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> future = pool.submit(() -> compute());
Integer result = future.get(5, TimeUnit.SECONDS);
pool.shutdown();
ФабрикаПоведение
newFixedThreadPool(n)Ровно n рабочих потоков, очередь задач
newCachedThreadPool()Потоки по требованию, риск раздувания при всплеске
newSingleThreadExecutor()Одна очередь, порядок FIFO

В production предпочтительно явно задавать размер пула и политику очереди (ThreadPoolExecutor), а не безлимитный cached.


CompletableFuture

Композиция асинхронных шагов без ручного wait/notify:

ExecutorService ioPool = Executors.newFixedThreadPool(8);

CompletableFuture<User> userFuture = CompletableFuture
.supplyAsync(() -> loadUser(id), ioPool);

CompletableFuture<List<Order>> ordersFuture = CompletableFuture
.supplyAsync(() -> loadOrders(id), ioPool);

CompletableFuture<Profile> profileFuture = userFuture
.thenCombine(ordersFuture, Profile::new);

Profile profile = profileFuture
.orTimeout(3, TimeUnit.SECONDS)
.exceptionally(ex -> Profile.empty())
.join();

Полезные методы:

  • thenApply / thenAccept / thenRun — продолжение с результатом / без / void;
  • thenCompose — когда следующий шаг тоже возвращает CompletableFuture;
  • allOf / anyOf — ожидание нескольких задач;
  • completeOnTimeout, orTimeout — дедлайны.

Обработка ошибок: необработанное исключение завершает цепочку; используйте exceptionally, handle или whenComplete.

future.handle((result, ex) -> {
if (ex != null) {
log.error("failed", ex);
return fallback;
}
return result;
});

Не вызывайте join()/get() на UI-потоке и не блокируйте event loop без причины.


Virtual threads (Java 21+)

Виртуальный поток — лёгкий поток JVM, монтируемый на небольшой пул carrier-потоков ОС. Подходит для массового блокирующего I/O (HTTP-клиент, JDBC, файлы).

try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i ->
executor.submit(() -> handleRequest(i))
);
}

Или напрямую:

Thread.startVirtualThread(() -> process(socket));
МодельКогда
Platform threads + фиксированный пулCPU-bound, предсказуемая нагрузка, legacy
Virtual threadsМного одновременных блокирующих I/O операций
CompletableFuture + явный пулКомпозиция шагов, интеграция с async API
Reactive (WebFlux)End-to-end неблокирующий стек, backpressure

Virtual threads не ускоряют CPU-bound задачи: для вычислений по-прежнему нужен ограниченный пул platform threads или ForkJoinPool.


synchronized, volatile, Lock

Кратко (подробнее в JVM и потоки; демон-потоки и shutdown hook — там же):

  • synchronized — взаимное исключение и видимость для блока/метода;
  • volatile — видимость записи без атомарности составных операций;
  • ReentrantLock, ReadWriteLock — гибче, с tryLock и таймаутами;
  • java.util.concurrent.atomic — счётчики и ссылки без блокировок.

Асинхронность не отменяет необходимость синхронизации общего изменяемого состояния.


parallelStream и async

parallelStream() делит данные между потоками ForkJoinPool — не путать с асинхронным I/O. См. Stream API.


Практические правила

  1. Разделяйте пулы для CPU и для I/O.
  2. Всегда задавайте таймауты на внешние вызовы.
  3. Закрывайте ExecutorService (shutdown / shutdownNow) при остановке приложения.
  4. Логируйте необработанные исключения в whenComplete.
  5. Для Spring Boot 3.2+ рассмотрите virtual threads через spring.threads.virtual.enabled=true на совместимых стеках.

Частые ошибки

ОшибкаПоследствие
Новый Thread на каждый запросИсчерпание памяти и CPU
get() без таймаутаЗависший поток навсегда
Virtual threads для CPU-boundНе даёт выигрыша, нужен пул
Общий ArrayList без синхронизацииГонки и «случайные» баги

Что попробовать

  1. Один сервис с CompletableFuture.orTimeout(2, SECONDS).
  2. Spring Boot 3.2+: spring.threads.virtual.enabled=true и сравнить нагрузку.
  3. Диагностика JVM при «всё тормозит» под нагрузкой.

Дальше

JVM и потоки · Stream API · Spring · Kotlin coroutines


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).