CodeGym /Kursy /JAVA 25 SELF /Używanie Executora z wirtualnymi wątkami

Używanie Executora z wirtualnymi wątkami

JAVA 25 SELF
Poziom 57 , Lekcja 3
Dostępny

1. Krótko o najważniejszym

Jesteś już nieco zaznajomiony z klasami z pakietu java.util.concurrent, szczególnie z ExecutorService. To taki „menedżer zadań”: wysyłasz do niego pracę (np. przez submit()), a on sam decyduje, kiedy i którym wątkiem ją wykonać. Zazwyczaj pod spodem działa pula wątków o stałym rozmiarze, która oszczędza zasoby i nie tworzy nowego wątku dla każdego zadania.

Jednak wraz z wirtualnymi wątkami wszystko się zmienia! Teraz możesz pozwolić sobie na luksus: dla każdego zadania — osobny wątek, i nie musisz się obawiać, że JVM „pęknie” z przejedzenia.

Nowy sposób: Executors.newVirtualThreadPerTaskExecutor()

W Javie 21 pojawił się nowy sposób utworzenia ExecutorService, który uruchamia każde zadanie w osobnym wirtualnym wątku:

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

Kluczowa różnica:

  • Stare pule wątków (Executors.newFixedThreadPool, Executors.newCachedThreadPool) ograniczały liczbę równoczesnych zadań ze względu na kosztowność wątków systemu operacyjnego.
  • Nowy wirtualny Executor jest niemal nieograniczony: dla każdego zadania — własny lekki wirtualny wątek.

Prosty przykład

Wyślijmy 10 zadań do wirtualnego Executora:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class VirtualExecutorDemo {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

        for (int i = 1; i <= 10; i++) {
            int taskId = i; // przechwytujemy zmienną dla lambdy
            executor.submit(() -> {
                System.out.println("Task " + taskId + " is running in thread: " +
                        Thread.currentThread());
            });
        }

        executor.shutdown();
    }
}

Co się dzieje?
Każde zadanie zostanie uruchomione we własnym wirtualnym wątku i zobaczysz linie w stylu:

Task 1 is running in thread: VirtualThread[#24]/runnable@ForkJoinPool-1-worker-1
...

2. Masowa równoległość: tysiące zadań — żaden problem!

Aby poczuć pełnię możliwości wirtualnych wątków, spróbujmy wysłać do ExecutorService nie 10, lecz, powiedzmy, 100_000 zadań. W klasycznych pulach byłoby to jak próba wciśnięcia słonia do lodówki: JVM szybko skończyłaby pamięć albo zaczęłaby strasznie zwalniać. Z wirtualnymi wątkami — jest inaczej!

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class VirtualExecutorMassiveDemo {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

        for (int i = 1; i <= 100_000; i++) {
            int taskId = i;
            executor.submit(() -> {
                // Dla przykładu — po prostu śpimy 1 ms
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                // System.out.println("Task " + taskId + " done."); // Nie wypisujemy, bo byłoby zbyt wiele linii!
            });
        }

        executor.shutdown();
    }
}

Uwaga: wypisywanie 100_000 wierszy na ekran to zły pomysł: konsola „zadławi się” szybciej niż wirtualne wątki. Lepiej albo nie pisać na konsolę wcale, albo wypisywać tylko pierwszych kilka zadań.

3. Jak działa newVirtualThreadPerTaskExecutor

W skrócie: ten ExecutorService tworzy nowy wirtualny wątek dla każdego zadania, które do niego wysyłasz. W odróżnieniu od stałej puli, nie ma tu kolejki zadań ani sztywnych ograniczeń liczby jednoczesnych wątków (poza limitami twojej JVM i sprzętu).

Architektonicznie:

  • Wirtualne wątki są „mapowane” na niewielką pulę rzeczywistych wątków systemowych (carrier).
  • JVM samodzielnie decyduje, kiedy i który wirtualny wątek uruchomić, wstrzymać i wznowić.
  • Jeśli wątek się blokuje (np. podczas czytania pliku albo oczekiwania na sieć), JVM może „zamrozić” wirtualny wątek i zwolnić wątek nośny (carrier) dla innych zadań.

4. Przykład: obsługa wyników z Future

ExecutorService zwraca obiekt typu Future, jeśli zadanie zwraca wynik. Wszystko działa tak samo jak przy zwykłych wątkach:

import java.util.concurrent.*;

public class VirtualExecutorWithResult {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

        Future<String> future = executor.submit(() -> {
            Thread.sleep(500);
            return "Hello from virtual thread!";
        });

        System.out.println("Result: " + future.get()); // Czekamy na wynik

        executor.shutdown();
    }
}

Wszystko znajome: można wysyłać zadania zwracające wartość, czekać na wynik przez get(), a wyjątki są obsługiwane standardowo.

5. Jak poprawnie zakończyć pracę Executor

Bardzo ważne jest, aby nie zapominać o zakończeniu pracy ExecutorService, żeby program nie zawisł (nawet jeśli wątki są wirtualne, a nie „prawdziwe”).

shutdown() i awaitTermination

executor.shutdown(); // Mówimy: nie przyjmujemy więcej zadań
executor.awaitTermination(1, TimeUnit.MINUTES); // Czekamy na zakończenie wszystkich zadań (maksymalnie 1 minuta)

Dlaczego to ważne?
Jeśli nie wywołasz shutdown(), to wirtualne wątki mogą dalej żyć i program nie zakończy się nawet po wykonaniu main(). To typowy błąd początkujących.

6. Przydatne niuanse

Porównanie: wirtualny Executor vs klasyczna pula wątków

Klasyczna pula (newFixedThreadPool) Wirtualny Executor (newVirtualThreadPerTaskExecutor)
Liczba wątków Ograniczona rozmiarem puli Jeden wirtualny wątek na zadanie, niemal bez limitu
Zadania w kolejce Tak, jeśli wszystkie wątki są zajęte Z reguły nie: zadanie od razu dostaje wątek
Koszt wątku Wysoki (stos, zasoby systemu operacyjnego) Bardzo niski (planowanie po stronie JVM)
Skalowalność Ograniczona Niemal nieograniczona
Do czego się nadaje Zadania CPU-bound, ograniczony paralelizm Zadania I/O-bound, masowy paralelizm

Integracja z serwerami WWW

Współczesne serwery WWW (np. Tomcat, Jetty, Undertow) zaczynają już wspierać wirtualne wątki. Oznacza to, że można obsługiwać każde żądanie HTTP w osobnym wirtualnym wątku, nie bojąc się „zadławić” przy napływie użytkowników.

Zaleta: nie trzeba wymyślać złożonych schematów asynchronicznych z callbackami i CompletableFuture; kod staje się prostszy — można pisać zwykły, blokujący kod, a aplikacja i tak się skaluje.

Masowe testowanie i symulacja obciążenia

Wirtualne wątki świetnie nadają się do testów, w których trzeba „zasymulować” tysiące jednoczesnych użytkowników, żądań lub operacji. Na przykład test wysyłający 10_000 równoległych żądań do serwera, każde w swoim wirtualnym wątku.

Równoległe przetwarzanie plików i połączeń sieciowych

Jeśli aplikacja pracuje z dużą liczbą plików lub połączeń sieciowych, możesz obsługiwać każde połączenie w osobnym wirtualnym wątku, bez martwienia się o ręczne zarządzanie pulami.

7. Typowe błędy przy pracy z wirtualnymi Executorami

Błąd nr 1: zapomniano wywołać shutdown(). Jeśli nie zamkniesz Executora, program się nie zakończy — wirtualne wątki wciąż będą oczekiwać nowych zadań. W razie potrzeby dodaj awaitTermination(...).

Błąd nr 2: używanie wirtualnych wątków do ciężkich obliczeń. Wirtualne wątki nie przyspieszają zadań w pełni obciążających CPU. Dla zadań CPU-bound lepiej użyć stałej puli (Executors.newFixedThreadPool) i starannie dobrać jej rozmiar.

Błąd nr 3: ignorowanie wyjątków wewnątrz zadań. Jeśli zadanie rzuci wyjątek, nie trafi on do wątku głównego — obsługuj go przez Future (metoda get()) albo przez try/catch wewnątrz lambdy.

Błąd nr 4: mylenie starej i nowej składni/wersji JDK. Upewnij się, że używasz właściwej wersji JDK (Java 21+) i że IDE jest skonfigurowane do obsługi wirtualnych wątków. Konkretna metoda — Executors.newVirtualThreadPerTaskExecutor().

Błąd nr 5: poleganie na ThreadLocal do przekazywania kontekstu. Wirtualne wątki często są tworzone i niszczone; ThreadLocal może zachowywać się inaczej, niż oczekujesz. Do przekazywania kontekstu używaj ScopedValue (Scoped Values; więcej szczegółów — w następnej lekcji).

1
Zadanie
JAVA 25 SELF, poziom 57, lekcja 3
Niedostępne
Transmisja danych z sondy międzyplanetarnej 🛰️
Transmisja danych z sondy międzyplanetarnej 🛰️
1
Zadanie
JAVA 25 SELF, poziom 57, lekcja 3
Niedostępne
Monitorowanie linii montażowej robotów-manipulatorów 🦾
Monitorowanie linii montażowej robotów-manipulatorów 🦾
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION