CodeGym /Kursy /JAVA 25 SELF /Uruchamianie wątków: Thread i Runnable, składnia

Uruchamianie wątków: Thread i Runnable, składnia

JAVA 25 SELF
Poziom 51 , Lekcja 1
Dostępny

1. Klasa Thread: pierwszy wątek w Javie

W Javie każdy wątek wykonania jest reprezentowany przez obiekt klasy Thread. Ta klasa — jak dyrygent w orkiestrze: to ona zarządza uruchomieniem, zatrzymaniem i cyklem życia wątku.

Aby uruchomić wątek, należy:

  1. Utworzyć obiekt klasy Thread (lub jej potomka).
  2. Wywołać na tym obiekcie metodę start().

Zobaczmy to w praktyce.

Przykład 1. Dziedziczenie po Thread

Najbardziej bezpośredni sposób utworzenia wątku to dziedziczyć własną klasę po Thread i nadpisać jej metodę run(). Wszystko, co napiszesz wewnątrz run(), wykona się w osobnym wątku.

// Prosty wątek przez dziedziczenie
public class HelloThread extends Thread {
    @Override
    public void run() {
        System.out.println("Witaj z wątku! Nazywam się: " + getName());
    }
}

public class Main {
    public static void main(String[] args) {
        HelloThread thread = new HelloThread(); // tworzymy obiekt-wątek
        thread.start(); // uruchamiamy wątek
        System.out.println("Wątek główny kończy pracę.");
    }
}

Co tak naprawdę się dzieje?
Gdy wywołujesz thread.start(), Java tworzy nowy wątek i dopiero w nim uruchamia metodę run(). W tym czasie wątek główny (ten, który rozpoczął się od main) nie czeka, tylko kontynuuje pracę równolegle.

I najważniejsze ostrzeżenie: nie myl start() z bezpośrednim wywołaniem run(). Jeśli napiszesz thread.run(), żaden nowy wątek nie powstanie — to zwykła metoda, która wykona się w tym samym wątku, w którym ją wywołano. Prawdziwa wielowątkowość zaczyna się dopiero od start().

Czym jest getName()?

Metoda getName() zwraca nazwę wątku. Domyślnie Java nadaje wątkom nazwy w stylu "Thread-0", "Thread-1" itd. To wygodne do debugowania.

2. Interfejs Runnable: najlepszy sposób w większości przypadków

Java to język, który lubi elastyczność. Klasa Thread już dziedziczy po Object, a w Javie nie ma wielokrotnego dziedziczenia klas. Jeśli twoja klasa będzie dziedziczyć po Thread, nie będziesz mógł dziedziczyć po niczym innym (na przykład, jeśli masz własną klasę Car i chcesz, by była wątkiem — musisz wybierać).

Rozwiązanie: przenieść logikę wątku do osobnego obiektu implementującego interfejs Runnable. To interfejs z jedną metodą run(). Następnie tworzysz obiekt klasy Thread, przekazujesz mu swój Runnable — i uruchamiasz wątek.

Przykład 2. Klasa z Runnable

// Klasa implementująca Runnable
public class HelloRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Witaj z Runnable! Wątek: " + Thread.currentThread().getName());
    }
}

public class Main {
    public static void main(String[] args) {
        Runnable runnable = new HelloRunnable();
        Thread thread = new Thread(runnable); // przekazujemy Runnable do Thread
        thread.start();
        System.out.println("Wątek główny kończy pracę.");
    }
}

Tutaj tworzymy klasę HelloRunnable, która implementuje interfejs Runnable. W metodzie main tworzymy obiekt tej klasy i przekazujemy go do konstruktora Thread. Następnie wątek uruchamiamy za pomocą start().

Warto osobno wspomnieć o metodzie Thread.currentThread(). To metoda statyczna, która pozwala uzyskać obiekt bieżącego wątku i tym samym zrozumieć, gdzie dokładnie wykonuje się nasz kod.

Dlaczego to lepsze?

  • Twoja klasa może dziedziczyć po dowolnej innej klasie (a nie tylko po Thread).
  • Można ponownie użyć tego samego Runnable do uruchomienia w wielu wątkach.
  • Kod staje się bardziej elastyczny i czystszy.

3. Składnia: klasy anonimowe i wyrażenia lambda

Java nie stoi w miejscu! Od Java 8 możemy pisać jeszcze krócej i wygodniej.

Przykład 3. Klasa anonimowa

Czasem nie chcemy tworzyć osobnego pliku dla klasy potrzebnej tylko raz. Możemy zadeklarować ją w miejscu użycia — to właśnie klasa anonimowa.

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Anonimowy Runnable! Wątek: " + Thread.currentThread().getName());
            }
        });
        thread.start();
        System.out.println("Wątek główny kończy pracę.");
    }
}

Przykład 4. Wyrażenie lambda (Java 8+)

Interfejs Runnable jest funkcyjny (ma tylko jedną metodę abstrakcyjną). Dlatego można użyć wyrażenia lambda:

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("Wątek lambda! Wątek: " + Thread.currentThread().getName());
        });
        thread.start();
        System.out.println("Wątek główny kończy pracę.");
    }
}

W skrócie:
Runnable jest implementowany „na miejscu”, a wyrażenie lambda dostarcza implementację metody run().

4. Uruchamianie wielu wątków

Można uruchomić dowolną liczbę wątków (no, prawie — wszystko zależy od zasobów komputera).

Przykład 5. Uruchamiamy kilka wątków

public class Main {
    public static void main(String[] args) {
        for (int i = 1; i <= 3; i++) {
            int threadNumber = i; // koniecznie zrób kopię zmiennej na potrzeby lambdy!
            Thread thread = new Thread(() -> {
                System.out.println("Wątek #" + threadNumber + ": cześć!");
            });
            thread.start();
        }
        System.out.println("Wątek główny kończy pracę.");
    }
}

Wynik może być różny!
Wątki pracują równolegle — to, który pierwszy wypisze swoją wiadomość, zależy od systemu operacyjnego i planisty wątków.

5. Cykl życia wątku

Zrozumienie, jak „żyje” wątek, jest ważne dla debugowania i poprawnego użycia wątków.

Podstawowe stany wątku

  • NEW — wątek utworzony, ale jeszcze nie uruchomiony (new Thread(...)).
  • RUNNABLE — wątek uruchomiony (start()), może się wykonywać.
  • TERMINATED — wątek zakończył działanie (metoda run() się zakończyła).

Co się dzieje przy wywołaniu start() i run()

  • start() — tworzy nowy wątek i wywołuje jego metodę run() w tym nowym wątku.
  • run() — zwykła metoda; jeśli wywołać ją bezpośrednio, wykona się w bieżącym wątku (nie utworzy nowego wątku!).

Przykład:

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> System.out.println("run() z wątku: " + Thread.currentThread().getName()));
        thread.run();   // WYWOŁYWANE W WĄTKU GŁÓWNYM!
        thread.start(); // WYWOŁYWANE W ODDZIELNYM WĄTKU!
    }
}

6. Praktyka: tworzymy proste wielowątkowe aplikacje

Kontynuujmy rozwój naszej aplikacji edukacyjnej — załóżmy, że piszemy prosty system logowania, w którym każdy wątek wypisuje własną wiadomość.

public class LoggerTask implements Runnable {
    private final String message;

    public LoggerTask(String message) {
        this.message = message;
    }

    @Override
    public void run() {
        System.out.println("[" + Thread.currentThread().getName() + "] Log: " + message);
    }
}

public class Main {
    public static void main(String[] args) {
        Thread logger1 = new Thread(new LoggerTask("Uruchomienie systemu"));
        Thread logger2 = new Thread(new LoggerTask("Wczytywanie danych"));
        Thread logger3 = new Thread(new LoggerTask("Przetwarzanie żądania"));

        logger1.start();
        logger2.start();
        logger3.start();

        System.out.println("Wątek główny kończy pracę.");
    }
}

Co się dzieje?
Trzy wątki równolegle wypisują swoje komunikaty na konsolę. Kolejność komunikatów nie jest gwarantowana — na tym polega wielowątkowość.

7. Przydatne uwagi

Tabela: porównanie sposobów tworzenia wątków

Sposób Elastyczność Możliwość ponownego użycia Zalecane?
Dziedziczenie po Thread Niska Nie Tylko do prostych/edukacyjnych przykładów
Implementacja Runnable Wysoka Tak Tak
Klasa anonimowa Średnia Nie Do jednorazowych zadań
Wyrażenie lambda Wysoka Nie Tak (Java 8+)

Schemat: jak działa uruchomienie wątku

+----------------------+ 
|  main (wątek główny) |
+----------------------+
           |
           v
+----------------------+
|  Thread thread = ... |
+----------------------+
           |
           v
+----------------------+
|  thread.start()      |  ← Wątek "ożywa"
+----------------------+
           |
           v
+----------------------+
|  run() w nowym wątku |
+----------------------+
           |
           v
+----------------------+
|  Wątek zakończony    |
+----------------------+

8. Typowe błędy przy uruchamianiu wątków

Błąd nr 1: Wywołanie run() zamiast start().
Bardzo częsty błąd początkujących — wywołać metodę run() bezpośrednio na obiekcie wątku. W takim przypadku żaden nowy wątek nie powstaje, metoda po prostu wykona się w bieżącym (głównym) wątku. Poprawnie: zawsze używaj start() do uruchomienia wątku.

Błąd nr 2: Ponowne uruchomienie wątku.
Nie wolno wywołać start() na tym samym obiekcie Thread więcej niż raz — spowoduje to IllegalThreadStateException. Jeśli musisz uruchomić zadanie ponownie, utwórz nowy obiekt Thread.

Błąd nr 3: Zmiana wspólnych zmiennych bez synchronizacji.
Jeśli kilka wątków uzyskuje dostęp do tych samych zmiennych, wynik może być nieprzewidywalny (warunki wyścigu, ang. race condition). O synchronizacji porozmawiamy później, ale na razie — zachowaj ostrożność.

Błąd nr 4: Brak uwzględnienia zakończenia wątków.
Jeśli ważne jest, aby poczekać na zakończenie wątku, użyj metody join(). W przeciwnym razie wątek główny może zakończyć się wcześniej niż wątki potomne.

Błąd nr 5: Mylenie nazwy wątku z nazwą klasy.
Metoda getName() zwraca nazwę wątku, a nie nazwę klasy. Do debugowania warto jawnie nadawać wątkom nazwy przez konstruktor lub setName().

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION