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:
- Utworzyć obiekt klasy Thread (lub jej potomka).
- 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().
GO TO FULL VERSION