1. Jak tworzyć wirtualne wątki w praktyce
Czas przejść od teorii do praktyki! Już wiesz, jak tworzy się wątek w klasyczny sposób:
Thread t = new Thread(() -> System.out.println("Hello from thread!"));
t.start();
Albo nieco krócej:
new Thread(() -> System.out.println("Hi!")).start();
Od Java 21 mamy nowy sposób:
Thread.startVirtualThread(() -> System.out.println("Hello from virtual thread!"));
albo bardziej jawnie:
Thread t = Thread.ofVirtual().start(() -> System.out.println("Hello from virtual thread!"));
Na czym polega różnica?
- Thread.ofVirtual().start(...) tworzy wirtualny wątek (Virtual Thread), zarządzany przez JVM, a nie przez system operacyjny.
- Thread.ofPlatform().start(...) (lub new Thread(...)) — klasyczny wątek, jak dotychczas.
Dlaczego to jest ważne?
Wirtualne wątki można tworzyć w dziesiątkach tysięcy, nie obawiając się OutOfMemoryError. Teraz, jeśli nagle postanowisz obsłużyć milion żądań — Java powie: „Żaden problem, dawaj więcej!”
2. Składnia tworzenia wirtualnego wątku
Przykład podstawowy:
public class VirtualThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.ofVirtual().start(() -> {
System.out.println("Pozdrowienia z wirtualnego wątku! Wątek: " + Thread.currentThread());
});
// Czekamy na zakończenie wątku (żeby main nie zakończył się wcześniej)
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Co się dzieje?
- Tworzymy wirtualny wątek za pomocą Thread.ofVirtual().start(...).
- Wewnątrz wątku — prosta czynność: wypisanie komunikatu.
- Na końcu wywołujemy thread.join(), aby wątek główny poczekał na zakończenie wirtualnego (w przeciwnym razie program może się zakończyć, zanim wątek zdąży coś wypisać).
Zwróć uwagę:
Wirtualny wątek wygląda i zachowuje się niemal jak zwykły, ale w środku — magia JVM!
3. Masowe tworzenie wirtualnych wątków: moc Loom w praktyce
Spróbujmy teraz czegoś, co z wątkami platformowymi byłoby ryzykowne (albo wręcz niemożliwe): utworzymy 10_000 wirtualnych wątków, z których każdy wypisze swój numer.
public class VirtualThreadMassive {
public static void main(String[] args) throws InterruptedException {
int N = 10_000;
Thread[] threads = new Thread[N];
for (int i = 0; i < N; i++) {
int threadNum = i;
threads[i] = Thread.ofVirtual().start(() -> {
System.out.println("Wirtualny wątek #" + threadNum + " działa!");
});
}
// Czekamy na zakończenie wszystkich wątków
for (Thread t : threads) {
t.join();
}
System.out.println("Wszystkie wirtualne wątki zakończone!");
}
}
- Dla zwykłych wątków (new Thread(...)) taki kod niemal na pewno „wywróci” twój program z OutOfMemoryError.
- Dla wątków wirtualnych — to normalny tryb! JVM z łatwością obsłuży tysiące i dziesiątki tysięcy wątków.
A tak przy okazji: jeśli 10_000 wydaje ci się dużo, spróbuj 100_000 lub nawet 1_000_000. Na współczesnej maszynie JVM da radę, jeśli twoje wątki wykonują prostą pracę lub czekają na operacje I/O.
4. Runnable i wyrażenia lambda: jak przekazywać kod wirtualnemu wątkowi
Wątki wirtualne przyjmują zadania tak samo jak wątki zwykłe: przez interfejs Runnable. Oznacza to, że możesz przekazywać i wyrażenia lambda, i referencję do metody, i dowolny obiekt implementujący Runnable.
Przykład z lambdą:
Thread.ofVirtual().start(() -> System.out.println("Lambda w wirtualnym wątku!"));
Przykład z metodą:
public class TaskRunner {
public static void main(String[] args) {
Thread.ofVirtual().start(TaskRunner::doWork);
}
static void doWork() {
System.out.println("Pracujemy w wirtualnym wątku: " + Thread.currentThread());
}
}
Przykład z klasą anonimową:
Thread.ofVirtual().start(new Runnable() {
@Override
public void run() {
System.out.println("Anonimowa klasa w wirtualnym wątku!");
}
});
Wniosek:
Wszystko, co działało z wątkami zwykłymi, działa też z wirtualnymi — tylko teraz jest to „lekko i szybko”.
5. Porównanie z ExecutorService: stare i nowe podejście
Klasyczny ExecutorService
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
int taskNum = i;
executor.submit(() -> {
System.out.println("Zadanie #" + taskNum + " wykonywane");
});
}
executor.shutdown();
Problem:
Jeśli zadań jest zbyt wiele, a wątków mało — zadania czekają w kolejce. Jeśli wątków jest zbyt dużo — program „zadławi się” z powodu braku zasobów.
Nowy sposób: executor na wątkach wirtualnych
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 0; i < 100_000; i++) {
int taskNum = i;
executor.submit(() -> {
System.out.println("Wirtualne zadanie #" + taskNum);
});
}
executor.shutdown();
Co się dzieje?
- Dla każdego zadania tworzony jest osobny wątek wirtualny.
- JVM sama zarządza ich planowaniem, nie przeciążając systemu.
- Nie ma potrzeby ograniczać rozmiaru puli — wątki wirtualne są „prawie darmowe”.
Kiedy używać executora na wątkach wirtualnych?
- Gdy masz duży strumień zadań i nie chcesz myśleć o rozmiarze puli.
- Gdy zadania są niezależne i mogą wykonywać się równolegle.
- Gdy zależy ci na prostocie: nie musisz ręcznie zarządzać wątkami.
6. Praktyczne wskazówki: kiedy czego używać
Kiedy używać Thread.ofVirtual().start() bezpośrednio?
- Gdy trzeba utworzyć osobny wątek dla unikalnego zadania (np. test, demonstracja lub prosty eksperyment).
- Gdy liczba wątków jest niewielka i chcesz zarządzać nimi ręcznie.
Kiedy używać Executors.newVirtualThreadPerTaskExecutor()?
- Gdy trzeba masowo uruchamiać zadania (np. obsługa dużej liczby żądań, plików, połączeń sieciowych).
- Gdy zadania są niezależne i nie wymagają wzajemnej koordynacji.
- Gdy chcesz włączyć wątki wirtualne do istniejącej architektury, w której już używasz ExecutorService (np. w serwerze WWW, procesorze zadań itd.).
Wskazówka:
Jeśli nie masz pewności — zacznij od executora na wątkach wirtualnych. To najbardziej uniwersalne i nowoczesne podejście.
7. Obsługa wyjątków w wątkach wirtualnych
Z punktu widzenia try-catch wątki wirtualne to zwykłe wątki. Jeśli wewnątrz twojego Runnable wystąpi wyjątek, nie „wywróci” on całej JVM, a po prostu zakończy dany wątek błędem.
Przykład:
Thread t = Thread.ofVirtual().start(() -> {
throw new RuntimeException("Coś poszło nie tak!");
});
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Wątek główny kontynuował pracę.");
W ExecutorService:
Jeśli wysyłasz zadanie przez submit, wynik możesz odebrać przez Future, a wyjątek zostanie zgłoszony przy wywołaniu get():
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Future<?> f = executor.submit(() -> {
throw new RuntimeException("Błąd w wirtualnym zadaniu");
});
try {
f.get();
} catch (ExecutionException e) {
System.out.println("Przechwycono błąd z wątku wirtualnego: " + e.getCause());
}
executor.shutdown();
8. Typowe błędy przy tworzeniu wątków wirtualnych
Błąd nr 1: Mylenie wątków wirtualnych z platformowymi. Jeśli tworzysz wątki przez new Thread(...) lub Thread.ofPlatform(), to nie są wątki wirtualne. Tylko Thread.ofVirtual().start(...) lub metody z Executors dają prawdziwe Virtual Threads.
Błąd nr 2: Oczekiwanie przyspieszenia dla ciężkich obliczeń. Wątki wirtualne nie przyspieszają zadań mocno obciążających procesor (CPU-bound). Jeśli masz milion wątków, z których każdy liczy liczbę Pi do milionowego miejsca po przecinku — JVM nie „przyspieszy” obliczeń, tylko będzie przełączać wątki.
Błąd nr 3: Trzymanie zasobów (np. baz danych) per wątek. Jeśli utworzysz milion wątków wirtualnych, ale każdy potrzebuje osobnego połączenia z bazą — baza nie wytrzyma. Wątki wirtualne są dobre dla zadań, w których większość czasu to oczekiwanie (I/O), a nie praca z ograniczonymi zewnętrznymi zasobami.
Błąd nr 4: Nieczekanie na zakończenie wątków, gdy to istotne. Jeśli wątek główny zakończy się wcześniej niż wątki wirtualne — program może się zakończyć bez doczekania wyników. Używaj join() lub ExecutorService z shutdown() i awaitTermination().
Błąd nr 5: Używanie przestarzałych bibliotek niekompatybilnych z wątkami wirtualnymi. Niektóre biblioteki zewnętrzne mogą blokować wątki na poziomie systemu operacyjnego lub używać natywnej synchronizacji, co obniża efektywność wątków wirtualnych. Zawsze sprawdzaj kompatybilność.
GO TO FULL VERSION