← Назад к списку
ТехническаяJava и KotlinSenior

Что такое виртуальные потоки в Java 21+? Чем они отличаются от платформенных и когда их использовать?

Короткий ответ

  • Виртуальный поток — лёгкий поток JVM, не привязанный к потоку ОС намертво
  • При блокирующем IO он отмонтируется от carrier-потока, освобождая его
  • Можно создавать миллионы — стек растёт динамически в куче
  • Выигрыш для IO-bound нагрузки, для CPU-bound выгоды нет
  • Пиннинг: synchronized с блокировкой внутри до Java 24 держал carrier
  • Пулы виртуальных потоков не нужны — поток на задачу
  • ThreadLocal работает, но на миллионах потоков дорог — есть ScopedValue

Виртуальные потоки позволяют писать простой блокирующий код с масштабируемостью асинхронного — для IO-bound сервисов.

Как сказать вслух

пример ответа

Виртуальный поток — это лёгкий поток, которым управляет сама JVM, а не операционная система. Когда он блокируется на вводе-выводе, JVM снимает его с несущего потока ОС и ставит туда другой — поэтому их можно создавать миллионами и писать обычный блокирующий код, который масштабируется как асинхронный. Смысл есть для IO-нагрузки: много запросов к базе и внешним API. Для чисто вычислительных задач выгоды нет — ядер больше не становится.

Подробный ответ

Основной ответ

Виртуальные потоки (стабильны с Java 21) — это user-mode потоки: JVM мультиплексирует их на небольшой пул carrier-потоков (ForkJoinPool по числу ядер). При блокирующей операции (сокеты, JDBC, sleep) виртуальный поток размонтируется, его стек сохраняется в куче, а carrier выполняет другие потоки. Это убирает главное ограничение модели «поток на запрос» — дороговизну потоков ОС, без переписывания кода на реактивщину. Создание: Thread.ofVirtual().start(...) или Executors.newVirtualThreadPerTaskExecutor(); пулить их не нужно — они одноразовые и дешёвые. Ограничения: пиннинг — до Java 24 блокировка внутри synchronized-секции удерживала carrier (лечится ReentrantLock, в новых версиях JDK исправлено); нативные вызовы тоже пиннят. Для передачи контекста на больших количествах потоков вместо ThreadLocal появился ScopedValue. Spring Boot включает их флагом spring.threads.virtual.enabled.

Ключевые моменты

  • Mount/unmount. Блокирующий вызов снимает поток с carrier; стек живёт в куче и растёт по необходимости.
  • Где выигрыш. IO-bound: тысячи параллельных запросов к БД/HTTP; CPU-bound ограничен числом ядер как и раньше.
  • Пиннинг. synchronized + блокировка внутри до JDK 24 держали carrier; диагностика -Djdk.tracePinnedThreads.
  • Практика использования. Не пулить, поток на задачу; осторожно с ThreadLocal-кэшами и ограничением конкурентности — нужен семафор, а не пул.

Практический контекст

Актуальный senior-вопрос: проверяют, следит ли кандидат за платформой и понимает ли, что виртуальные потоки — альтернатива реактивному стеку, а не ускоритель всего. Практические темы рядом: пулы соединений БД становятся узким местом (виртуальных потоков тысячи, коннектов десятки), ограничение параллелизма семафором, совместимость со старыми библиотеками, использующими synchronized. Хороший ответ сравнивает: простота блокирующего кода против сложности CompletableFuture/WebFlux.

Пример кода

// Поток на задачу: 10_000 параллельных блокирующих вызовов
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    List<Future<String>> results = IntStream.range(0, 10_000)
        .mapToObj(i -> executor.submit(() -> httpClient.send(
                request(i), HttpResponse.BodyHandlers.ofString())
            .body()))
        .toList();
}
// Ограничение конкурентности — семафором, не пулом:
// Semaphore permits = new Semaphore(100);

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

  • Ожидают ускорения CPU-bound задач от виртуальных потоков
  • Создают пул виртуальных потоков фиксированного размера, теряя весь смысл
  • Не знают про пиннинг на synchronized и получают деградацию на старых библиотеках

ИП Кочкин Алексей Сергеевич · ИНН 390509026279 · ОГРНИП 325390000030973 · jiniys2005@yandex.ru