Что такое 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), растёт/уменьшается динамически.
Частые ошибки:
- Pinning —
synchronizedс блокирующими операциями внутри. Решение — заменить на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.