Що таке Virtual Threads в Java 21?
Virtual Threads (віртуальні потоки) — це легкі потоки, керовані JVM, а не операційною системою. Вони стали стабільною фічею в Java 21 (LTS-версія), але розроблялися в рамках Pro...
Virtual Threads (віртуальні потоки) — це легкі потоки, керовані JVM, а не операційною системою. Вони стали стабільною фічею в Java 21 (LTS-версія), але розроблялися в рамках Project Loom з 2017 року і проходили через кілька preview-релізів (Java 19-20).
Важливе застереження: хоча API стабільний у Java 21, деякі пов’язані фічі (Scoped Values, Structured Concurrency) все ще в preview. Це означає, що у майбутніх версіях їх API може змінитися. Якщо ви використовуєте лише базовий Thread.ofVirtual(), це production-ready.
Junior рівень
Базове розуміння
Virtual Threads (віртуальні потоки) — це легкі потоки, керовані JVM, а не операційною системою. Вони дозволяють створювати мільйони потоків замість тисяч.
Чому звичайні потоки дорогі: кожен Platform Thread резервує ~1MB стеку в пам’яті ОС при створенні, навіть якщо використовує лише кілька кілобайт. Крім того, перемикання між потоками вимагає переходу в режим ядра (kernel mode), що займає тисячі CPU-циклів.
Чому віртуальні потоки дешеві: їх стек зберігається у звичайній купі JVM (heap) і росте динамічно — від кількох кілобайт. Перемикання відбувається в user-space без системних викликів, що в 100-1000 разів швидше.
Проста аналогія
- Platform Threads (звичайні): як таксі — кожне вимагає машину (1MB пам’яті)
- Virtual Threads: як маршрутне таксі — багато пасажирів на одному транспорті
Створення та використання
// Platform Thread (старий спосіб)
Thread platform = new Thread(() -> {
System.out.println("Platform thread");
});
platform.start();
// Virtual Thread (Java 21+)
Thread virtual = Thread.ofVirtual().start(() -> {
System.out.println("Virtual thread");
});
// Або через білдер
Thread vThread = Thread.startVirtualThread(() -> {
System.out.println("Virtual thread");
});
Порівняння
| Характеристика | Platform Threads | Virtual Threads |
|---|---|---|
| Ким керується | ОС (Kernel) | JVM (Runtime) |
| Розмір стеку | ~1 MB (статичний) | Кілька KB (динамічний) |
| Час створення | ~1-10 ms | ~мікросекунди |
| Максимум | Тисячі | Мільйони |
Приклад: Thread-per-request
// З віртуальними потоками — проста модель знову працює!
ServerSocket server = new ServerSocket(8080);
while (true) {
Socket client = server.accept();
Thread.ofVirtual().start(() -> {
handleRequest(client); // Один віртуальний потік на запит
});
}
// Мільйони з'єднань — без реактивного програмування!
Middle рівень
Архітектура: Continuations
В основі віртуальних потоків лежить концепція Continuations (продовжень):
1. Віртуальний потік виконує код на Carrier Thread (реальний потік ОС)
2. При блокуючій операції (sleep, I/O):
a. JVM зберігає стек віртуального потоку в купу (Heap)
b. Потік "відчіпляється" (unmount) від Carrier Thread
c. Carrier Thread йде виконувати інший віртуальний потік
3. Коли I/O завершується:
a. JVM відновлює стек з купи
b. Потік "причіпляється" (mount) до вільного Carrier Thread
c. Продовжує виконання з місця зупинки
Carrier Thread Pool
// Віртуальні потоки ділять реальні потоки (Carrier Threads)
// За замовчуванням: ForkJoinPool з розміром = число ядер CPU
// Приклад: 1,000,000 віртуальних потоків на 8 Carrier Threads
// Кожен Carrier Thread обробляє ~125,000 віртуальних потоків
// по черзі, коли вони розмонтуються
Механізм Stack Chunking
JVM зберігає стек віртуального потоку у вигляді об’єктів StackChunk у купі:
Віртуальний потік:
┌────────────────────────┐
│ StackChunk #1 (у купі) │ ← Поточний стек
│ StackChunk #2 (у купі) │ ← Попередній фрейм
│ StackChunk #3 (у купі) │ ← Ще глибше
└────────────────────────┘
При монтуванні дані копіюються з купи в реальний стек процесора, при розмонтуванні — назад.
Створення пулу віртуальних потоків
// ExecutorService для віртуальних потоків
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1_000_000; i++) {
executor.submit(() -> {
// Обробляємо задачу
Thread.sleep(1000); // Розмонтується — не блокує Carrier
});
}
}
// Мільйон задач — і JVM не впала!
Senior рівень
Under the Hood: Pinning (Пригвождення)
Це головна пастка віртуальних потоків. Віртуальний потік може “прилипнути” до Carrier Thread і не розмонтуватися при блокуванні:
Причини Pinning
| Причина | Опис | Рішення |
|---|---|---|
| synchronized блок/метод | Віртуальний потік у synchronized НЕ розмонтується | Замінити на ReentrantLock |
| Native методи (JNI) | Виклики нативного коду не підтримують unmount | Обгорнути в окремий потік |
// ПОГАНО: synchronized блокує Carrier Thread
synchronized(lock) {
Thread.sleep(10000); // Віртуальний потік "прилип" до носія на 10 секунд!
}
// ГАРНО: ReentrantLock підтримує unmount
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
Thread.sleep(10000); // Розмонтується — Carrier Thread вільний
} finally {
lock.unlock();
}
Діагностика Pinning
// Обов'язковий флаг при розробці з Virtual Threads
java -Djdk.tracePinnedThreads=full MyApp
// Виведе stack trace щоразу, коли потік "прилипає"
ThreadLocal та Virtual Threads
// ОБЕРЕЖНО: мільйон VT = мільйон копій ThreadLocal!
ThreadLocal<ExpensiveObject> local = ThreadLocal.withInitial(ExpensiveObject::new);
// Рішення: Scoped Values (Java 21 Preview)
static final ScopedValue<UserContext> CONTEXT = new ScopedValue<>();
ScopedValue.runWhere(CONTEXT, new UserContext("user123"), () -> {
// CONTEXT.get() доступний всередині
});
// Після виходу — автоматично очищається, немає витоків
Продуктивність та Highload
Thread-per-request повертається
До VT:
1000 platform threads max → Reactive/CompletableFuture → Callback Hell
З VT:
1,000,000 virtual threads → Thread-per-request → Простий блокуючий код
Spring Boot 3.2+
# application.yml
spring:
threads:
virtual:
enabled: true
# Весь Tomcat/Undertow переходить на віртуальні потоки!
Carrier Thread Pool sizing
// За замовчуванням: ForkJoinPool.commonPool()
// Розмір = число логічних ядер CPU
// Кастомізація:
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "16");
Діагностика
JFR Events
java -XX:StartFlightRecording=filename=rec.jfr MyApp
Події:
jdk.VirtualThreadStart/jdk.VirtualThreadEndjdk.VirtualThreadPinned— критично для моніторингу
jcmd для дампів
# jstack може "збентежитися" від мільйона потоків
jcmd <pid> Thread.dump_to_file -format=json threads.json
# JSON формат ефективніший для аналізу
Memory Analysis
Мільйон віртуальних потоків = мільйон об'єктів у купі
Кожен VT ~ кількох KB (стек) + об'єкти
Перевірте -Xmx перед запуском!
Рекомендується: мінімум 256MB для мільйона VT
Коли VT НЕ дають переваги
| Сценарій | Чому | Альтернатива |
|---|---|---|
| CPU-bound задачі | VT не розмонтуються, оверхед планувальника | FixedThreadPool (N+1) |
| synchronized-heavy код | Pinning — блокують Carrier Threads | ReentrantLock |
| Обмеження ресурсів | Мільйон VT покладе БД | Semaphore |
Best Practices
- Використовуйте VT для I/O-bound — веб-сервери, API-шлюзи, проксі
- Замінюйте synchronized на ReentrantLock — уникнення pinning
- Використовуйте Semaphore — для обмеження доступу до дефіцитних ресурсів
- Обережно з ThreadLocal — мільйон копій = багато пам’яті
- Scoped Values — для передачі контексту (Java 21 Preview)
- -Djdk.tracePinnedThreads=full — при розробці
- Не використовуйте VT для CPU-bound — FixedThreadPool краще
- Моніторте jdk.VirtualThreadPinned — через JFR
Коли НЕ використовувати Virtual Threads
- Java нижче 21 — VT недоступні. Для Java 17 використовуйте реактивний стек (WebFlux, CompletableFuture)
- CPU-bound задачі (обчислення, ML, криптографія) — VT не розмонтуються при обчисленнях, лише додають оверхед планувальника. Використовуйте
Executors.newFixedThreadPool(N)де N = число ядер + 1 - synchronized-heavy legacy-код — pinning заблокує Carrier Threads. Спочатку рефакторіть на ReentrantLock
- Обмежені ресурси (пул БД на 20 з’єднань) — мільйон VT покладе БД. Використовуйте Semaphore
- ThreadLocal-heavy додатки — мільйон VT = мільйон копій ThreadLocal = OutOfMemoryError. Переходьте на Scoped Values
Virtual Threads vs Platform Threads vs ExecutorService: що обрати?
| Ситуація | Вибір | Чому |
|---|---|---|
| Веб-сервер, API-шлюз (I/O-bound) | Virtual Threads | Мільйони одночасних з’єднань, простий код |
| Обчислення (ML, парсинг) | FixedThreadPool | VT не розмонтуються, оверхед без користі |
| Legacy з synchronized | FixedThreadPool + рефакторинг | Pinning VT вб’є продуктивність |
| Обмежений ресурс (БД, API ліміт) | VT + Semaphore | VT масштабуються, Semaphore обмежує |
🎯 Шпаргалка для інтерв’ю
Обов’язково знати:
- Virtual Threads (VT) — легкі потоки, керовані JVM, а не ОС; стек у купі (динамічно), а не нативний стек (статичний 1MB)
- При блокуючій I/O VT “розмонтується” — JVM зберігає стек у StackChunk і віддає Carrier Thread іншому VT
- Carrier Thread Pool за замовчуванням = ForkJoinPool з розміром = число ядер CPU
- Pinning: synchronized і native методи не дозволяють VT розмонтуватися — Carrier Thread заблокований; рішення: ReentrantLock
- ThreadLocal + мільйон VT = OutOfMemoryError; альтернатива: Scoped Values (Java 21 Preview)
- VT для I/O-bound (веб-сервери, API), НЕ для CPU-bound (обчислення)
- Spring Boot 3.2+:
spring.threads.virtual.enabled=true
Часті уточнюючі запитання:
- Чому VT не підходять для CPU-bound задач? — VT не розмонтуються при обчисленнях, лише додають оверхед планувальника JVM
- Що таке StackChunk? — Об’єкт у купі, що зберігає стек віртуального потоку; при монтуванні копіюється в реальний стек процесора
- Як діагностувати pinning? — Флаг
-Djdk.tracePinnedThreads=fullвиводить stack trace при кожному випадку пригвождення - Скільки VT можна створити? — Мільйони; обмеження — heap (рекомендується мінімум 256MB для мільйона VT)
Червоні прапорці (НЕ говорити):
- ❌ “Virtual Threads — це те саме, що threads в Go” — VT схожі на goroutines, але працюють на JVM з Carrier Threads
- ❌ “VT завжди швидше Platform Threads” — VT швидше лише для I/O-bound; для CPU-bound VT повільніше через оверхед
- ❌ “Можна просто замінити всі synchronized на VT без змін” — synchronized викликає pinning, потрібно замінити на ReentrantLock
- ❌ “VT несумісні з ExecutorService” —
Executors.newVirtualThreadPerTaskExecutor()— стандартний спосіб роботи з VT
Пов’язані теми:
- [[24. В чому перевага Virtual Threads перед звичайними потоками]]
- [[25. Коли варто використовувати Virtual Threads]]
- [[26. Що таке structured concurrency]]
- [[27. В чому різниця між Thread та Runnable]]