CodeGym /Kursy /JAVA 25 SELF /Wątki kontra wątki wirtualne: różnice, zalety

Wątki kontra wątki wirtualne: różnice, zalety

JAVA 25 SELF
Poziom 57, Lekcja 0
Dostępny

1. Klasyczne wątki: jak to działa i gdzie boli

Najpierw przypomnijmy, jak działają zwykłe wątki w Javie (nazywane też wątkami platformowymi lub native threads). Te, które tworzy się przez new Thread(...).

Kiedy wywołujesz new Thread(() -> { ... }).start();, JVM nie po prostu uruchamia kawałek kodu. Prosi system operacyjny o utworzenie prawdziwego wątku wykonania. System operacyjny przydziela dla niego osobny stos (zwykle kilka megabajtów) i rezerwuje inne zasoby systemowe.

Taki wątek żyje, dopóki wykonuje swoje zadanie, i przez cały ten czas zajmuje miejsce w tabeli wątków systemu operacyjnego. Im więcej takich wątków, tym więcej pamięci idzie na ich stosy i tym większe obciążenie dla systemu. Dlatego przy dużej liczbie jednocześnie działających wątków aplikacja może zacząć „dusić się” — system poświęca zbyt dużo czasu na ich przełączanie.

Przykład: klasyczny wątek

Thread thread = new Thread(() -> {
    System.out.println("Witaj z wątku!");
});
thread.start();

Wygląda prosto, prawda? Ale spróbuj utworzyć nie jeden–dwa, lecz, powiedzmy, dziesięć tysięcy takich wątków — i twój program szybko zacznie się dusić. Albo skończy się pamięć, albo system powie, że limit wątków został wyczerpany. To nie błąd Javy, lecz naturalna konsekwencja architektury: wątki są ciężkie i kosztowne.

Dlaczego tak jest? Każdy wątek dostaje własny stos (zwykle 1–2 megabajty), plus cały zestaw struktur pomocniczych od systemu operacyjnego. A sam system operacyjny nie jest zachwycony, gdy podrzuca mu się dziesiątki tysięcy wątków — ma ograniczenia i bywają one dość surowe.

A nawet jeśli pamięć się nie skończy, pojawia się inny kłopot — przełączanie kontekstu. Gdy wątków jest zbyt wiele, system nieustannie skacze między nimi, zapisując i odtwarzając ich stan. To wszystko zajmuje czas i zjada wydajność, więc zysk z „masowego równoleglenia” się nie pojawia.

Problem „jeden wątek — jedno żądanie”

W starych aplikacjach serwerowych (np. na Tomcat lub Jetty) często stosowano model „thread‑per‑request”: dla każdego przychodzącego żądania użytkownika przydzielano osobny wątek. To wygodne, ale jeśli masz 10_000 użytkowników, potrzebujesz 10_000 wątków! Serwerowi robi się ciężko i zaczyna się wyścig o pamięć, a nie o szybkość obsługi żądań.

Wniosek:
Klasyczne wątki są dobre dla niewielkiej liczby równoległych zadań, ale nie skalują się do dziesiątek i setek tysięcy.

2. Czym są wątki wirtualne (Virtual Threads)?

Tu na scenę wchodzą bohaterowie dzisiejszego wykładu — wątki wirtualne. To nie po prostu „kolejny wątek”, lecz zupełnie inna koncepcja architektoniczna.

Wątki wirtualne to wątki, którymi zarządza nie system operacyjny, lecz sama JVM. Są zaimplementowane w całości w Javie i można je tworzyć w ogromnych ilościach (dziesiątki i setki tysięcy) bez „puchnięcia” pamięci i spowolnień.

Krótko:

  • Platform Thread (wątek platformowy): zwykły wątek, który bezpośrednio odpowiada wątkowi systemu operacyjnego.
  • Virtual Thread (wątek wirtualny): lekki wątek zarządzany przez JVM, a nie przez OS.

Jak to jest zbudowane?

Wątki wirtualne to takie „lekkie” wątki, które żyją nie w systemie operacyjnym, lecz wewnątrz samej JVM. Działają na wierzchu niewielkiej puli prawdziwych wątków, nazywanych platform threads. Można to sobie wyobrazić tak, że JVM zarządza nimi jak dyrygent orkiestrą: ma ograniczoną liczbę muzyków (prawdziwe wątki), ale umiejętnie rozdziela między nimi partie (wątki wirtualne).

Schemat architektury:

+-------------------+           +-------------------+
|  Virtual Thread 1 |---\       |  Platform Thread  |
|  Virtual Thread 2 |---->====> |  (Carrier Thread) |
|  Virtual Thread 3 |---/       +-------------------+
         ...                        (System operacyjny)

Carrier Thread — to zwykły wątek systemu operacyjnego, na którym JVM wykonuje wiele wątków wirtualnych. Jeśli jakiś wątek wirtualny nagle się zablokuje — na przykład czeka na dane z dysku lub z sieci — JVM po prostu go „zamraża”, zwalniając carrier thread dla innych zadań.

Dlaczego to rewolucyjne?

Bo teraz można pisać zwykły, liniowy kod — bez nieskończonych callbacków, CompletableFuture i piekielnych łańcuszków thenApply — a jednocześnie skalować aplikację do tysięcy równoczesnych operacji.

Wątki wirtualne zajmują zaledwie dziesiątki kilobajtów (zamiast megabajtów w zwykłych) i tworzą się praktycznie natychmiast. Dlatego można je uruchamiać i niszczyć tysiącami, nie obawiając się, że system operacyjny runie pod ich ciężarem. To sprawia, że programowanie równoległe w Javie wreszcie staje się lekkie i naturalne.

3. Zalety wątków wirtualnych

Skalowalność

Dzięki wątkom wirtualnym możesz sobie pozwolić na luksus uruchamiania dziesiątek i setek tysięcy równoległych zadań. Na przykład obsługiwać każde żądanie sieciowe w osobnym wątku — bez obaw, że serwer „pęknie”.

Demonstracja: 100 000 wątków wirtualnych

for (int i = 0; i < 100_000; i++) {
    Thread.ofVirtual().start(() -> {
        // Tutaj może być dowolna logika
        try {
            Thread.sleep(1000); // Symulacja pracy
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    });
}
System.out.println("Wszystkie wątki zostały uruchomione!");

Ten kod spokojnie wykonuje się na zwykłym laptopie!
Spróbuj zrobić to samo ze zwykłymi wątkami — i zobaczysz OutOfMemoryError albo dostaniesz „cegłę” zamiast komputera.

Prostota programowania

Wątki wirtualne pozwalają pisać zwykły „blokujący” kod, nie zamieniając go w „spaghetti” z asynchronicznych wywołań. Na przykład możesz spokojnie używać Thread.sleep, InputStream.read, Socket.accept — JVM sama zadba o to, by nie blokować całego carrier thread.

Lepsza czytelność i utrzymanie kodu

Zamiast skomplikowanych schematów z callbackami i CompletableFuture piszesz kod liniowy, zrozumiały. To zmniejsza liczbę błędów i ułatwia utrzymanie.

Nie trzeba odkrywać koła na nowo

Dawniej, aby obsłużyć tysiące żądań równolegle, trzeba było używać asynchronicznych frameworków, bibliotek reaktywnych (Netty, Vert.x, Project Reactor), które wymagają szczególnego stylu programowania. Teraz można się bez nich obejść — i mimo to uzyskać skalowalność.

4. Architektura: jak wątki wirtualne działają „pod maską”

Mapowanie na carrier threads

JVM tworzy niewielką pulę prawdziwych wątków (carrier threads) — zwykle tyle, ile jest rdzeni procesora. Wszystkie wątki wirtualne „jeżdżą” na tych carrier threads, jak pasażerowie autobusami.

  • Gdy wątek wirtualny się blokuje (np. czeka na odpowiedź z sieci), JVM „zdejmuje” go z carrier thread i odkłada do kolejki.
  • Gdy tylko wątek może kontynuować pracę, JVM ponownie „wsadza” go na wolny carrier thread.

Analogia:
Wyobraź sobie, że masz 4 taksówki (carrier threads) i obsługujesz 10 000 klientów (virtual threads). Gdy tylko jeden klient dojedzie i wysiądzie, taksówka natychmiast zabiera następnego. Nikt nie stoi bezczynnie, a taksówki nie łamią się pod ciężarem pasażerów.

Planowanie i przełączanie

JVM samodzielnie decyduje, który wątek wirtualny wykonywać w danej chwili. Jeśli wątek blokuje się na I/O, nie przeszkadza innym wątkom w pracy.

5. Ograniczenia i cechy wątków wirtualnych

Nie wszystko złoto, co wirtualne

Nie do długich obliczeń: Jeśli masz zadanie, które stale okupuje procesor (heavy CPU‑bound), wątek wirtualny nie da przyrostu wydajności. Po prostu dlatego, że carrier threads i tak są ograniczone liczbą rdzeni.

Niektóre blokady są nieefektywne: Stare mechanizmy synchronizacji (np. synchronizacja przez synchronized na obiektach z natywnymi muteksami) mogą nie pozwalać JVM „zamrozić” wątku wirtualnego. W takich przypadkach carrier thread będzie czekał razem z wątkiem wirtualnym, zmniejszając skalowalność.

Nie wszystkie biblioteki lubią się z wątkami wirtualnymi: Jeśli biblioteka wykonuje wywołania natywne lub używa specyficznych blokad, wątki wirtualne mogą zachowywać się inaczej, niż oczekujesz.

Przykład: kiedy nie warto używać Virtual Threads

Jeśli masz zadanie, które kręci się w nieskończonej pętli i liczy liczby, wątek wirtualny nie da żadnego zysku. I tak uderzamy w liczbę rdzeni.

Thread.ofVirtual().start(() -> {
    while (true) {
        // Liczymy w nieskończoność
    }
});

Wynik:
Jeden carrier thread będzie zajęty tym wątkiem wirtualnym, a inne zadania będą czekać swojej kolejki.

6. Porównanie: Platform Thread vs Virtual Thread

Cecha Platform Thread (zwykły) Virtual Thread (wirtualny)
Zarządzany przez system operacyjny JVM
Pamięć na wątek Megabajty Dziesiątki kilobajtów
Liczba wątków Zwykle < 10_000 Tysiące, setki tysięcy
Koszt utworzenia Drogo Tanie
Skalowalność Ograniczona Prawie nieograniczona
Odpowiedni dla Długich zadań, CPU‑bound Krótkich zadań I/O‑bound
Przełączanie wątków OS JVM
Zgodność 100% Prawie zawsze, ale są niuanse

7. Przykład: jak wyglądałby serwer przed i po wprowadzeniu Virtual Threads

Przed (Platform Threads)

ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
    Socket client = serverSocket.accept();
    new Thread(() -> handleClient(client)).start();
}

Problem:
Po 5 000 połączeń serwer zacznie się „krztusić”.

Po (Virtual Threads, Java 21+)

ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
    Socket client = serverSocket.accept();
    Thread.ofVirtual().start(() -> handleClient(client));
}

Magia:
Teraz można obsługiwać dziesiątki tysięcy połączeń — i nie myśleć o limitach wątków!

9. Typowe błędy przy przejściu na wątki wirtualne

Błąd nr 1: Oczekiwanie przyspieszenia dla zadań obliczeniowych. Wątki wirtualne nie przyspieszają zadań, które w pełni obciążają procesor. Dla takich zadań nadal ogranicza nas liczba rdzeni.

Błąd nr 2: Używanie starych blokujących synchronizacji. Jeśli używasz starych blokad (np. synchronized na obiektach, które mogą być „zablokowane” natywnie), wątki wirtualne mogą nie być zdejmowane z carrier thread, tracąc wszystkie korzyści.

Błąd nr 3: Nieuwzględnianie zachowania bibliotek zewnętrznych. Niektóre biblioteki zewnętrzne mogą nie być gotowe na pracę z wątkami wirtualnymi (np. jeśli używają JNI lub natywnych blokad).

Błąd nr 4: Oczekiwanie magicznego wzrostu wydajności. Wątki wirtualne to nie panaceum. Nie przyspieszają wszystkiego, a jedynie czynią współbieżność tanią i wygodną dla zadań I/O‑bound.

1
Zadanie
JAVA 25 SELF, poziom 57, lekcja 0
Niedostępne
Uruchomienie sekretnego komunikatora 💬
Uruchomienie sekretnego komunikatora 💬
1
Zadanie
JAVA 25 SELF, poziom 57, lekcja 0
Niedostępne
Przygotowanie do startu floty dronów 🚀
Przygotowanie do startu floty dronów 🚀
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION