Virtual Threads (Project Loom) to jedna z największych zmian w modelu współbieżności Javy od lat. Wprowadzone oficjalnie jako funkcja stabilna w Java 21 (LTS), pozwalają pisać kod w stylu klasycznego programowania blokującego, a jednocześnie skalować aplikację do setek tysięcy równoległych zadań.
W tym artykule pokazujemy:
- czym są Virtual Threads,
- kiedy mają sens w produkcji,
- proste przykłady kodu,
- typowe pułapki,
- linki do dokumentacji do dalszej nauki.
Dlaczego Virtual Threads są przełomem?
Tradycyjne wątki systemowe (platform threads):
- są ciężkie (każdy to osobny wątek OS),
- ograniczone liczbą zasobów systemowych,
- drogie w tworzeniu i przełączaniu kontekstu.
Virtual Threads:
- są zarządzane przez JVM, a nie bezpośrednio przez system operacyjny,
- są lekkie (kilkadziesiąt tysięcy to norma),
- świetnie nadają się do zadań I/O (HTTP, DB, kolejki, pliki).
Ciekawostka
W testach laboratoryjnych JVM potrafi obsłużyć miliony virtual threads, o ile nie blokują się one na zasobach systemowych w niekontrolowany sposób.
Najprostszy przykład: Platform Thread vs Virtual Thread
Klasyczny wątek (Java 8+)
Thread thread = new Thread(() -> {
System.out.println("Hello from platform thread");
});
thread.start();
Virtual Thread (Java 21+)
Thread.startVirtualThread(() -> {
System.out.println("Hello from virtual thread");
});
Lub z użyciem ExecutorService:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> {
System.out.println("Task on virtual thread");
});
}
Kod wygląda niemal identycznie, ale:
- platform thread = ciężki zasób systemowy,
- virtual thread = lekki byt zarządzany przez JVM.
Virtual Threads a aplikacje webowe (Spring / backend)
Największy zysk widać w aplikacjach typu:
- REST API,
- mikroserwisy,
- systemy integracyjne (HTTP, DB, messaging),
- aplikacje o dużej liczbie jednoczesnych requestów.
Przykład prostego kontrolera, który blokuje się na I/O:
@GetMapping("/data")
public String getData() throws InterruptedException {
Thread.sleep(1000); // symulacja I/O (np. DB, HTTP)
return "OK";
}
Na platform threads:
- każdy request = osobny wątek OS,
- szybki limit przy dużym ruchu.
Na virtual threads:
- każdy request = virtual thread,
- JVM sama mapuje je na pulę wątków systemowych.
Efekt: znacznie lepsza skalowalność przy tym samym kodzie.
Co działa świetnie z Virtual Threads?
Idealne przypadki:
- aplikacje I/O-bound (sieć, bazy danych, pliki),
- klasyczny kod blokujący (
sleep, JDBC, REST clients), - systemy z dużą liczbą jednoczesnych użytkowników.
Działa bez przepisywania aplikacji na reaktywną:
- nie trzeba przechodzić na WebFlux czy Reactor,
- zachowujemy prosty, synchroniczny styl kodu.
Czego unikać (ważne w produkcji)
1) Operacje CPU-bound
Virtual Threads nie przyspieszą:
- ciężkich obliczeń,
- algorytmów matematycznych,
- przetwarzania danych w pętli CPU.
Tu nadal potrzebujesz klasycznej kontroli puli wątków.
2) Blokowanie na niekompatybilnych bibliotekach
Niektóre stare biblioteki:
- używają natywnego kodu,
- blokują wątki w sposób, którego JVM nie potrafi „odczepić”.
Może to zniwelować korzyści z Virtual Threads.
3) Synchronizacja na dużą skalę
Kod w stylu:
synchronized (lock) {
Thread.sleep(1000);
}
to antywzorzec przy tysiącach virtual threads – blokujesz współdzielony zasób.
Praktyczny przykład: 100 000 zadań
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 100_000; i++) {
executor.submit(() -> {
Thread.sleep(1000);
return null;
});
}
}
Na platform threads: aplikacja szybko się „zadławi”. Na virtual threads: JVM spokojnie obsłuży taką liczbę zadań (o ile masz pamięć i nie blokujesz krytycznych sekcji).
Uwaga: ten sam kod z newFixedThreadPool(200) zachowa się zupełnie inaczej – zadania będą czekać w kolejce.
Czy Virtual Threads zastąpią programowanie reaktywne?
Nie do końca, ale zmieniają równowagę sił.
Reaktywność (Reactor, WebFlux):
- nadal świetna przy strumieniach danych,
- wymaga innego modelu myślenia.
Virtual Threads:
- pozwalają pisać prosty kod synchroniczny,
- osiągać podobną skalowalność w wielu przypadkach,
- są łatwiejsze do wdrożenia w istniejących systemach (legacy).
Dla wielu zespołów to ogromne uproszczenie architektury.
Checklista produkcyjna (przed wdrożeniem)
- [ ] Java 21 LTS
- [ ] Testy wydajnościowe (porównanie platform vs virtual threads)
- [ ] Sprawdzenie bibliotek (JDBC driver, HTTP client, SDK chmurowe)
- [ ] Monitoring: liczba wątków, heap, czasy odpowiedzi
- [ ] Brak krytycznych sekcji z
synchronizedna długie operacje - [ ] Fallback (łatwa możliwość powrotu do platform threads)
Oficjalne źródła i dokumentacja
- Project Loom (OpenJDK)
- Java 21 Virtual Threads (Oracle docs)
- JEP 444 – Virtual Threads
- Executors.newVirtualThreadPerTaskExecutor()
- Structured Concurrency (JEP 453)
Podsumowanie
Virtual Threads to realna zmiana paradygmatu:
- pozwalają skalować klasyczny kod blokujący,
- upraszczają architekturę backendów,
- redukują potrzebę skomplikowanych rozwiązań reaktywnych,
- są szczególnie wartościowe w systemach I/O-bound.
W praktyce produkcyjnej sprawdzają się najlepiej tam, gdzie:
- mamy dużo jednoczesnych requestów,
- dużo czekania na I/O,
- chcemy zachować czytelny, synchroniczny kod.
Jak wykorzystujemy Virtual Threads w R&D
W projektach R&D stosujemy Virtual Threads:
- w gatewayach API,
- w systemach integracyjnych,
- w batch processingu opartym o I/O,
- jako eksperymentalny replacement dla klasycznych thread pooli.
Każde wdrożenie poprzedzamy:
- testami obciążeniowymi,
- analizą bibliotek,
- monitoringiem po wdrożeniu.