1. Skalowalność
Dlaczego zwykłe wątki słabo się skalują?
Każdy klasyczny wątek (Thread) — to byt systemu operacyjnego z własnym stosem (zwykle 1–2 MB) i strukturą stanu. Próba utworzenia, powiedzmy, 10 000 zwykłych wątków nierzadko prowadzi do błędu OutOfMemoryError. Dlatego w tradycyjnych serwerach używa się ograniczonych pul wątków.
Wątki wirtualne: magia skalowalności
Wątki wirtualne (Java 21+) — „lekkie” wątki zarządzane przez JVM. Ich stos jest przechowywany na stercie i może dynamicznie rosnąć/kurczyć się. Gdy wątek blokuje się na I/O, JVM „zamraża” go i kontynuuje wykonywanie innych zadań.
Wewnątrz JVM działa niewielka pula „nośników” (carrier threads) — platformowych wątków systemu operacyjnego, na których kolejno wykonywane są wątki wirtualne. To pozwala tworzyć 100 000+ zadań bez grozy pamięciowej. JVM sama planuje, które wątki wirtualne i kiedy wykonywać.
Demonstracja: 100_000 wątków wirtualnych kontra 1_000 platformowych
Przykład: utworzenie 1000 zwykłych wątków
// Próba utworzenia 1000 zwykłych wątków
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
Thread t = new Thread(() -> {
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
threads.add(t);
t.start();
}
System.out.println("Utworzono wątków: " + threads.size());
Wynik: Na większości systemów uda się utworzyć 1 000–2 000 wątków; przy większych wartościach zaczną się problemy z pamięcią i spowolnienia.
Przykład: utworzenie 100 000 wątków wirtualnych
// Tworzymy 100_000 wątków wirtualnych
List<Thread> vThreads = new ArrayList<>();
for (int i = 0; i < 100_000; i++) {
Thread t = Thread.ofVirtual().start(() -> {
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
vThreads.add(t);
}
System.out.println("Utworzono wirtualnych wątków: " + vThreads.size());
Wynik: Program bez problemu tworzy 100 000 wątków wirtualnych bez awarii i istotnych spowolnień. Zużycie pamięci jest wielokrotnie mniejsze.
Porównanie wizualne
| Typ wątku | Maksymalna liczba wątków (w przybliżeniu) | Zużycie pamięci | Czas uruchomienia |
|---|---|---|---|
| Zwykłe (Thread) | 1 000 – 10 000 | Wysokie | Długo |
| Wirtualne | 100 000 – 1 000 000+ | Niskie | Natychmiast |
Fakt: Wątki wirtualne pozwalają pisać kod „jeden wątek na zadanie” bez złożonych pul i ryzyka przeciążenia systemu.
2. Wydajność: gdzie wątki wirtualne sprawdzają się
Zadania ograniczone przez I/O (I/O‑bound)
Wątki wirtualne świetnie nadają się do zapytań sieciowych, I/O plików i pracy z bazą danych. Gdy operacja się blokuje, wątek wirtualny zwalnia „nośnika”, a JVM wykonuje inne zadania. To zwiększa przepustowość przy dużej liczbie równoczesnych oczekiwań.
Przykład: symulacja 10 000 równoczesnych żądań HTTP
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
HttpClient client = HttpClient.newHttpClient();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10_000; i++) {
Thread t = Thread.ofVirtual().start(() -> {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com"))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Odpowiedź: " + response.statusCode());
} catch (Exception e) {
System.out.println("Błąd: " + e.getMessage());
}
});
threads.add(t);
}
// Czekamy na zakończenie wszystkich wątków
for (Thread t : threads) {
t.join();
}
Wynik: Wszystkie 10 000 żądań wykonują się równolegle; program nie ulega awarii, a kod pozostaje prosty.
CPU‑bound: wątki wirtualne nie przyspieszają obliczeń
Jeśli zadanie obciąża CPU, wątki wirtualne nie dodadzą szybkości: liczba rdzeni jest stała. Tutaj właściwe są stałe pule równe liczbie rdzeni, aby nie tworzyć zbędnej konkurencji.
// Każde zadanie oblicza sumę dużego zakresu
Runnable cpuTask = () -> {
long sum = 0;
for (int i = 0; i < 100_000_000; i++) {
sum += i;
}
System.out.println("Suma: " + sum);
};
// Uruchamiamy 1000 wirtualnych wątków z obliczeniami
for (int i = 0; i < 1000; i++) {
Thread.ofVirtual().start(cpuTask);
}
Wynik: Wątki będą konkurować o CPU, ale przyspieszenia nie będzie — to nie jest zadanie dla Virtual Threads.
3. Ograniczenia i cechy szczególne
Synchronizacja i pułapki wątków wirtualnych
- Uwaga na blokady natywne. Użycie synchronized może „przykleić” wątek wirtualny do nośnika, zmniejszając korzyści. Preferuj ReentrantLock, Semaphore i inne prymitywy z java.util.concurrent, zoptymalizowane pod wątki wirtualne.
- Starsze biblioteki. Niektóre sterowniki JDBC i biblioteki natywne nie są jeszcze zoptymalizowane pod Virtual Threads. Dokładnie testuj operacje blokujące.
Nie do zadań długotrwałych
Virtual Threads są idealne do „krótkich” jednostek pracy: obsługa żądania, jedna operacja i koniec. Milion wiecznie żyjących zadań (np. nieskończone obliczenia) nie przyniesie korzyści — dla nich użyj wątków platformowych.
4. Best practices: gdzie używać wątków wirtualnych
- Zadania I/O‑bound: wywołania sieciowe, pliki, bazy danych — wszędzie, gdzie wątek często czeka.
- Serwery WWW: obsługuj każde żądanie HTTP w osobnym wątku wirtualnym.
- Testy integracyjne: szybko zasymuluj tysiące klientów.
- Przetwarzanie asynchroniczne: pisz zwykły „blokujący” kod — JVM wykonuje sprytne planowanie.
Nie warto używać:
- Do zadań stale obciążających CPU.
- Gdy kluczowa jest zgodność z niskopoziomowymi bibliotekami (nie wszystkie są jeszcze zaadaptowane).
Pod spodem wygodnie użyć executora: Executors.newVirtualThreadPerTaskExecutor() — „jeden wirtualny wątek na zadanie”, bez stałej puli.
5. Monitorowanie i pomiary: jak zobaczyć wątki wirtualne w akcji
JVisualVM i Flight Recorder
JVisualVM pokaże aktywne wątki, ich stany i pamięć; począwszy od Java 21 wątki wirtualne są wyświetlane osobno. Java Flight Recorder (JFR) zapisuje szczegółową „czarną skrzynkę” wykonania, w tym statystyki dla Virtual Threads — wygodne do szukania wąskich gardeł.
Jak sprawdzić liczbę wątków w kodzie
Prosty sposób, aby zobaczyć liczbę wątków w JVM:
System.out.println("Łączna liczba wątków: " + Thread.activeCount());
Policzyć, ile z nich jest wirtualnych:
long vCount = Thread.getAllStackTraces().keySet().stream()
.filter(Thread::isVirtual)
.count();
System.out.println("Wirtualnych wątków: " + vCount);
6. Typowe błędy przy pracy z wątkami wirtualnymi
Błąd nr 1: Używanie wątków wirtualnych do ciężkich obliczeń. Uruchomienie milionów wątków wirtualnych z zadaniami CPU‑bound nie przyspieszy procesora. Virtual Threads to nie „turbo” do obliczeń.
Błąd nr 2: Ślepe kopiowanie starych wzorców. Nie twórz stałych pul wątków wirtualnych. Użyj Executors.newVirtualThreadPerTaskExecutor() i pozwól JVM skalować się automatycznie.
Błąd nr 3: Korzystanie z nieobsługiwanych bibliotek. Blokady natywne i biblioteki niezaadaptowane pod Loom mogą powodować zawieszenia i spadki wydajności. Sprawdź zgodność z wyprzedzeniem.
Błąd nr 4: Przedwczesna optymalizacja. Jeśli masz kilka wątków i zwykłą wielowątkowość — nie spiesz się z przenoszeniem wszystkiego na Virtual Threads. To narzędzie jest najlepsze tam, gdzie jest masowe I/O i oczekiwanie.
Błąd nr 5: Ignorowanie monitoringu. Utworzyć milion zadań jest łatwo, ale bez monitoringu i obsługi wyjątków można dostać „ładny benchmark” zamiast niezawodnego systemu. Używaj JVisualVM i JFR.
GO TO FULL VERSION