CodeGym /Kursy /JAVA 25 SELF /Tworzenie wirtualnych wątków: Thread.ofVirtual().start()

Tworzenie wirtualnych wątków: Thread.ofVirtual().start()

JAVA 25 SELF
Poziom 57 , Lekcja 1
Dostępny

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ść.

1
Zadanie
JAVA 25 SELF, poziom 57, lekcja 1
Niedostępne
Telemetria bolidu wyścigowego 🏎️
Telemetria bolidu wyścigowego 🏎️
1
Zadanie
JAVA 25 SELF, poziom 57, lekcja 1
Niedostępne
Zarządzanie eksperymentalnym robotem-asystentem 🤖
Zarządzanie eksperymentalnym robotem-asystentem 🤖
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION