← Все статьи

Project Loom в Java 23: как виртуальные потоки меняют серверный код

Если вы пишете на Java высоконагруженные серверные приложения, вы наверняка знакомы с классической дилеммой: блокирующий код прост для написания и понимания, но убивает производительность, в то время как асинхронные модели (CompletableFuture, реактивные стеки) дают масштабируемость ценой головной боли при разработке и отладке. К марту 2026 года эта многолетняя боль наконец-то получила своё лекарство — и оно стало стандартом. Речь о Project Loom, точнее, о его главном детище — виртуальных потоках (Virtual Threads), которые окончательно перешли из статуса preview-фичи в стабильный API в Java 21 и к сегодняшнему дню стали неотъемлемой частью экосистемы.

Вместо того чтобы обходить гору проблем асинхронности, Loom предлагает кардинально иное решение: сделать блокирующие операции дешёвыми. Традиционные потоки платформы (kernel threads) тяжеловесны. Их создание, переключение контекста и потребление памяти (стек ~1 Мб) — дорогие операции для ОС. Поэтому мы вынуждены использовать пулы с небольшим фиксированным числом потоков (например, 200), и если все они заблокированы в ожидании ответа от базы данных или внешнего API, приложение встаёт колом.

Виртуальные потоки — это легковесные потоки, управляемые не операционной системой, а самой JVM. Их можно создавать тысячами и даже миллионами. Потребление памяти начинается с нескольких килобайт, а переключение между ними происходит в пользовательском пространстве, что невероятно быстро. Для JVM виртуальный поток — это всего лишь объект Java. Магия заключается в том, что когда такой поток выполняет блокирующую операцию (например, чтение из сокета или sleep), рантайм Loom автоматически «отцепляет» его от несущего потока платформы (carrier thread), освобождая тот для выполнения другого виртуального потока. С точки зрения вашего кода ничего не меняется — вы всё так же пишете синхронный, последовательный код с привычными блоками try-catch.

Давайте рассмотрим практический пример из жизни микросервиса на Spring Boot 4.x (актуального на 2026 год). Представьте endpoint, который должен агрегировать данные из трёх различных внешних сервисов.

Старый подход с CompletableFuture выглядел бы так: public CompletableFuture<AggregatedData> getAggregatedData(String id) { CompletableFuture<ServiceAResponse> futureA = callServiceA(id); CompletableFuture<ServiceBResponse> futureB = callServiceB(id); CompletableFuture<ServiceCResponse> futureC = callServiceC(id);

return CompletableFuture.allOf(futureA, futureB, futureC).thenApply(v -> { try { return new AggregatedData(futureA.join(), futureB.join(), futureC.join()); } catch (CompletionException e) { throw new RuntimeException(e.getCause()); } }); } private CompletableFuture<ServiceAResponse> callServiceA(String id) { // Асинхронный HTTP-клиент... }

Код уже неочевиден, цепочки исключений требуют особой обработки, а stack trace при ошибке превращается в малопонятную пачку вызовов из пула fork-join.

Теперь посмотрим на реализацию с виртуальными потоками: public AggregatedData getAggregatedData(String id) throws InterruptedException { try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { Future<ServiceAResponse> futureA = executor.submit(() -> callServiceA(id)); Future<ServiceBResponse> futureB = executor.submit(() -> callServiceB(id)); Future<ServiceCResponse> futureC = executor.submit(() -> callServiceS(id));

return new AggregatedData(futureA.get(), futureB.get(), futureC.get()); } } private ServiceAResponse callServiceA(String id) { // Обычный синхронный HTTP-клиент (например, RestTemplate или новый синхронный HttpClient) // Блокирующий вызов здесь — это нормально! }

Код вернулся к линейной читаемости. Мы используем знакомый ExecutorService, но созданный через newVirtualThreadPerTaskExecutor(). Каждая задача выполняется в своём собственном виртуальном потоке. Метод future.get() блокирует текущий (также виртуальный) поток до получения результата, но поскольку это дешёвая блокировка, она не расходует критичные ресурсы ОС. Вся сложность управления скрыта внутри JVM.

Однако переход на Loom — это не просто поиск и замена «newFixedThreadPool» на «newVirtualThreadPerTaskExecutor». Чтобы получить максимальную пользу от этой технологии к 2026 году необходимо учесть несколько ключевых практических аспектов.

Во-первых,pinned virtual threads. Виртуальный поток может быть «прикреплён» (pinned) к платформенному потоку на время выполнения операции синхронизации с использованием native monitor (обычный synchronized блок или метод). На это время преимущество параллелизма теряется. // Потенциальная проблема: synchronized блок private final Object lock = new Object(); public void process() { synchronized(lock) { // Здесь виртуальный поток может быть "прикреплён" // Долгая операция внутри synchronized someBlockingIoOperation(); // Плохо! } } Решение — заменить synchronized на конструкции из java.util.concurrent.locks.ReentrantLock. private final ReentrantLock lock = new ReentrantLock(); public void process() { lock.lock(); try { someBlockingIoOperation(); // Теперь блокировка будет отпущена корректно } finally { lock.unlock(); } }

Во-вторых,misuse of thread pools. Виртуальные потоки создаются дёшево и предназначены для одноразового использования под задачу («один поток на одну задачу»). Использование их в пулах лишено смысла и является антипаттерном. // НЕ ДЕЛАЙТЕ ТАК: пул виртуальных потоков ExecutorService pooledVTExecutor = Executors.newFixedThreadPool(200); // ДЕЛАЙТЕ ТАК: исполнитель "задача-поток" ExecutorService vThreadPerTaskExecutor = Executors.newVirtualThreadPerTaskExecutor();

В-третьих,cost of context switching хоть и мал по сравнению с платформенными потоками, но всё же ненулевой внутри JVM. Если ваша задача чисто вычислительная (CPU-bound) без операций ввода-вывода, виртуальные потоки не дадут прироста производительности, а могут даже добавить небольшие накладные расходы. Идеальная область применения Loom — это типичные серверные задачи, где доля ожидания IO близка к 90% или выше.

Внедрение этой технологии также повлияло на экосистему. Популярные фреймворки, такие как Spring, в своих последних версиях предлагают seamless интеграцию. Например, в конфигурации веб-сервера Tomcat или Jetty можно просто установить свойство server.executor=virtual, чтобы каждый HTTP-запрос автоматически обрабатывался в отдельном виртуальном потоке. Это позволяет обслуживать десятки тысяч одновременных соединений без глубокого рефакторинга бизнес-логики под реактивную модель.

Таким образом, к весне 2026 года Project Loom перестал быть экспериментальной технологией «на будущее». Это рабочий инструмент, который позволяет писать высокопроизводительные серверные приложения, сохраняя простоту синхронной парадигмы. Главное правило при его использовании — доверять JVM управление параллелизмом: создавайте столько виртуальных потоков, сколько требуется задачам, избегайте long-held synchronized блоков и используйте современные синхронные клиенты для баз данных и HTTP. Это возвращает Java статус одного из самых продуктивных языков для создания понятного и одновременно масштабируемого backend -кода.

Заключение. Виртуальные потоки стирают границу между простотой разработки и высокой конкурентностью. Они представляют собой эволюционный скачок платформы Java, делающий сложные архитектурные паттерны доступными без чрезмерного усложнения кодовой базы. Для бизнеса это означает более низкую стоимость поддержки ПО и ускорение time-to-market для новых функций — ведь писать и отлаживать линейный код по - прежнему проще всего

💬 Комментарии (8)
👤
support.team24
23.03.2026 03:07
А как насчёт совместимости с популярными фреймворками, типа Spring WebFlux? Придётся ли отказываться от них?
👤
info.department2
24.03.2026 21:26
Работаю с высоконагруженным API. После перехода на виртуальные потоки в тестах latency упала значительно. Рекомендую попробовать.
👤
pavel.novikov99
27.03.2026 09:14
Статья хорошая, но хотелось бы увидеть конкретный бенчмарк сравнения пропускной способности с обычными потоками и реактивным подходом.
👤
feedback-office
29.03.2026 23:47
Спасибо за статью! А можно подробнее про отладку таких приложений? Становятся ли stack traces читаемыми?
👤
igor.novikov
30.03.2026 00:04
Слишком оптимистично звучит. Не приведет ли массовое создание виртуальных потоков к новым проблемам, например, с потреблением памяти?
👤
feedback-office
30.03.2026 01:18
Отличное объяснение сути проблемы. Теперь понятно, почему Loom — это действительно game-changer для серверной разработки на Java.
👤
stanislav.belov
30.03.2026 07:37
Интересно, а насколько сложно будет переписать существующий сервис с CompletableFuture на виртуальные потоки? Есть ли подводные камни?
👤
frank.jones-tech
04.04.2026 04:20
Наконец-то! Ждал этого с тех пор, как Loom был в раннем доступе. Простота блокирующего кода с производительностью реактивщины — это мечта.