Gymterview
middle

Что такое Virtual Threads (Project Loom) и чем они отличаются от Platform Threads?

Virtual Threads (виртуальные потоки) — это легковесные потоки, появившиеся в Java 21 (JEP 444) как стабильная функциональность. Они управляются JVM, а не операционной системой, и позволяют создавать миллионы потоков одновременно с минимальными затратами.

Platform Threads — это традиционные потоки Java, являющиеся тонкой обёрткой над потоками ОС. Каждый занимает ~1 МБ стека и требует системных ресурсов для создания и переключения контекста.

Сравнительная таблица:

Характеристика Platform Thread Virtual Thread
Управление ОС JVM
Потребление памяти ~1 МБ на стек ~несколько КБ (динамический рост)
Максимальное количество Тысячи Миллионы
Стоимость создания Высокая Очень низкая
Переключение контекста Дорогое (ОС) Дешёвое (JVM)
Привязка к ОС-потоку 1:1 M:N (множество на нескольких ОС-потоках)
Стек Нативная память (фиксированный) Heap (динамический)

Как работают Virtual Threads:

Виртуальные потоки работают поверх пула потоков-носителей (carrier threads), которые являются обычными Platform Threads. Когда виртуальный поток выполняет блокирующую операцию (IO, sleep, Lock), JVM автоматически «отсоединяет» (unmount) его от носителя, позволяя последнему выполнять другой виртуальный поток.

Код: создание и использование виртуальных потоков
// Создание виртуального потока
Thread vThread = Thread.ofVirtual().name("my-vthread").start(() -> {
    System.out.println("Работаю в виртуальном потоке: " + Thread.currentThread());
});

// Фабрика виртуальных потоков
ThreadFactory factory = Thread.ofVirtual().name("worker-", 0).factory();

// Executor с виртуальными потоками — по одному потоку на задачу
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    List<Future<String>> futures = new ArrayList<>();
    for (int i = 0; i < 100_000; i++) {
        final int taskId = i;
        futures.add(executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1)); // не блокирует ОС-поток!
            return "Результат задачи " + taskId;
        }));
    }
    for (Future<String> future : futures) {
        System.out.println(future.get());
    }
}
Код: HTTP-сервер на виртуальных потоках
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    ServerSocket serverSocket = new ServerSocket(8080);
    while (true) {
        Socket socket = serverSocket.accept();
        executor.submit(() -> handleRequest(socket));
    }
}

void handleRequest(Socket socket) {
    try (var in = socket.getInputStream(); var out = socket.getOutputStream()) {
        byte[] data = in.readAllBytes();       // JVM: unmount virtual thread
        String response = processRequest(data); // может обращаться к БД, API
        out.write(response.getBytes());
    }
}

Когда использовать Virtual Threads:

  • IO-bound задачи: HTTP-запросы, обращения к БД, файловый ввод-вывод
  • Задачи с большим количеством одновременных операций ожидания
  • Замена thread-per-request модели в серверных приложениях

Когда НЕ использовать:

  • CPU-bound задачи — виртуальные потоки не дают преимущества
  • Задачи, требующие привязки к ОС-потоку (JNI, GPU)
  • Код с synchronized и длительными блокировками внутри — происходит pinning

Ключевые факты:

  • Virtual Threads всегда демоны — они не предотвращают завершение JVM.
  • Нет смысла создавать пул фиксированного размера — паттерн «один поток на задачу» является нормой.
  • Полностью совместимы с существующим API: Thread, ExecutorService, Lock.
  • Стек хранится в куче (heap), растёт/уменьшается динамически.

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

  • Pinningsynchronized с блокирующими операциями внутри. Решение — заменить на ReentrantLock.
  • Пулирование виртуальных потоков (newFixedThreadPool) — антипаттерн.
  • Хранение больших объектов в ThreadLocal при миллионах потоков — утечка памяти. Используйте ScopedValue.

Актуальность: Spring Boot 3.2+ поддерживает через spring.threads.virtual.enabled=true. Quarkus, Micronaut, Tomcat, Jetty имеют встроенную поддержку. JDBC-драйверы совместимы с виртуальными потоками.

Аналогия: Platform Threads — это такси: каждая машина (ОС-поток) перевозит одного пассажира (задачу). Можно иметь лишь ограниченный автопарк. Virtual Threads — это автобусная система: один автобус (carrier thread) перевозит множество пассажиров, высаживая одних (unmount при блокировке) и подбирая других (mount при готовности).

На собеседовании ключевой вопрос: «Что такое pinning и как его избежать?» Ответ: pinning — это ситуация, когда виртуальный поток не может быть отсоединён от carrier thread, потому что находится внутри synchronized-блока с блокирующей операцией. Решение: заменить synchronized на ReentrantLock.