Jeśli chcesz, żeby Twoja aplikacja Java robiła kilka rzeczy jednocześnie – na przykład pobierała dane z internetu podczas gdy użytkownik wciąż może klikać w interfejs – potrzebujesz multithreadingu. To jedna z najważniejszych umiejętności każdego Java developera.
Dlaczego to ważne?
Współczesne aplikacje muszą być responsywne. Użytkownicy nie chcą czekać aż jedna operacja się skończy, żeby móc kliknąć przycisk. Multithreading pozwala aplikacji „robić kilka rzeczy na raz” – interfejs pozostaje żywy podczas gdy w tle wykonują się czasochłonne operacje jak dostęp do bazy danych czy wywołania API.
Co się nauczysz:
- Czym są wątki (threads) i jak działają w Javie
- Jak tworzyć wątki używając klasy Thread i interfejsu Runnable
- Podstawowe metody zarządzania wątkami
- Jakie problemy może powodować współdzielenie danych między wątkami
- Kiedy używać multithreadingu w praktycznych aplikacjach
Wymagania wstępne:
Poziom: Podstawy programowania Java
Potrzebujesz: Znajomość klas, obiektów, interfejsów w Javie
Środowisko: Java 8+ (przykłady działają w każdej wersji Java)
Czym jest wątek (thread)?
Wątek (thread) to niezależna ścieżka wykonania w programie. Każdy program Java ma co najmniej jeden wątek – main thread, w którym wykonuje się metoda main(). Możemy tworzyć dodatkowe wątki, które będą działać równolegle.
Tworzenie wątków – sposób pierwszy: klasa Thread
Najprostszy sposób na stworzenie wątku to rozszerzenie klasy Thread:
public class MyThread extends Thread { private String threadName; public MyThread(String name) { this.threadName = name; } @Override public void run() { // Ten kod będzie wykonywany w osobnym wątku for (int i = 1; i <= 5; i++) { System.out.println(threadName + " - krok " + i); try { // Pauza 1 sekunda Thread.sleep(1000); } catch (InterruptedException e) { System.out.println(threadName + " został przerwany"); } } System.out.println(threadName + " zakończony!"); } }
Jak używać tego wątku:
public class ThreadExample { public static void main(String[] args) { // Tworzymy dwa wątki MyThread thread1 = new MyThread("Wątek-1"); MyThread thread2 = new MyThread("Wątek-2"); // Uruchamiamy je thread1.start(); // Ważne: start(), nie run()! thread2.start(); System.out.println("Main thread kończy pracę"); } }
Tworzenie wątków - sposób drugi: interfejs Runnable
Lepszym podejściem jest implementacja interfejsu Runnable. Dlaczego? Bo Java ma tylko dziedziczenie pojedyncze, więc jeśli nasza klasa już dziedziczy po innej klasie, nie możemy rozszerzyć Thread.
public class MyTask implements Runnable { private String taskName; public MyTask(String name) { this.taskName = name; } @Override public void run() { for (int i = 1; i <= 5; i++) { System.out.println(taskName + " - wykonuję krok " + i); try { Thread.sleep(800); } catch (InterruptedException e) { System.out.println(taskName + " przerwane"); return; } } System.out.println(taskName + " zakończone!"); } }
Używanie z Runnable:
public class RunnableExample { public static void main(String[] args) { // Tworzymy zadania MyTask task1 = new MyTask("Zadanie-A"); MyTask task2 = new MyTask("Zadanie-B"); // Tworzymy wątki przekazując zadania Thread thread1 = new Thread(task1); Thread thread2 = new Thread(task2); // Uruchamiamy thread1.start(); thread2.start(); System.out.println("Zadania zostały uruchomione"); } }
Podstawowe metody zarządzania wątkami
Metody klasy Thread
Metoda | Opis | Przykład użycia |
---|---|---|
start() | Uruchamia wątek | Zawsze używaj zamiast run() |
sleep(ms) | Wstrzymuje wątek na określony czas | Symulacja długiej operacji |
join() | Czeka aż wątek się zakończy | Synchronizacja zakończenia |
isAlive() | Sprawdza czy wątek jeszcze działa | Monitorowanie stanu |
interrupt() | Przerywa wątek | Graceful shutdown |
Praktyczny przykład z join()
public class JoinExample { public static void main(String[] args) { Thread worker = new Thread(() -> { System.out.println("Pracownik rozpoczyna pracę..."); try { Thread.sleep(3000); // 3 sekundy pracy } catch (InterruptedException e) { System.out.println("Praca przerwana"); } System.out.println("Pracownik kończy pracę"); }); worker.start(); try { // Czekamy aż worker skończy worker.join(); } catch (InterruptedException e) { System.out.println("Oczekiwanie przerwane"); } System.out.println("Wszyscy pracownicy skończyli - zamykamy biuro"); } }
Problem współdzielenia danych
Zobaczmy problem na przykładzie:
public class CounterProblem { private static int counter = 0; public static void main(String[] args) throws InterruptedException { // Dwa wątki będą zwiększać ten sam licznik Thread thread1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { counter++; // Niebezpieczne! } }); Thread thread2 = new Thread(() -> { for (int i = 0; i < 1000; i++) { counter++; // Niebezpieczne! } }); thread1.start(); thread2.start(); // Czekamy aż oba się skończą thread1.join(); thread2.join(); System.out.println("Spodziewany wynik: 2000"); System.out.println("Rzeczywisty wynik: " + counter); // Często będzie mniej niż 2000! } }
Podstawowe rozwiązanie - synchronized
Najprostsze rozwiązanie to słowo kluczowe synchronized:
public class SafeCounter { private static int counter = 0; private static final Object lock = new Object(); public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { synchronized (lock) { counter++; // Teraz bezpieczne } } }); Thread thread2 = new Thread(() -> { for (int i = 0; i < 1000; i++) { synchronized (lock) { counter++; // Teraz bezpieczne } } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("Wynik: " + counter); // Zawsze 2000 } }
Kiedy używać multithreadingu?
Dobre przypadki użycia:
Praktyczny przykład: pobieranie danych
public class DataFetcher implements Runnable { private String dataSource; public DataFetcher(String source) { this.dataSource = source; } @Override public void run() { System.out.println("Rozpoczynam pobieranie z: " + dataSource); try { // Symulacja pobierania danych (np. z API) Thread.sleep(2000 + (int)(Math.random() * 1000)); } catch (InterruptedException e) { System.out.println("Pobieranie przerwane: " + dataSource); return; } System.out.println("✓ Dane pobrane z: " + dataSource); } public static void main(String[] args) throws InterruptedException { long startTime = System.currentTimeMillis(); // Pobieramy dane z trzech źródeł równocześnie Thread api1 = new Thread(new DataFetcher("API-1")); Thread api2 = new Thread(new DataFetcher("API-2")); Thread api3 = new Thread(new DataFetcher("API-3")); api1.start(); api2.start(); api3.start(); // Czekamy na wszystkie api1.join(); api2.join(); api3.join(); long endTime = System.currentTimeMillis(); System.out.println("Wszystkie dane pobrane w: " + (endTime - startTime) + "ms"); System.out.println("Sekwencyjnie zajęłoby to ~6-9 sekund!"); } }
start() tworzy nowy wątek i wywołuje run() w tym wątku. Bezpośrednie wywołanie run() wykonuje kod w bieżącym wątku - bez multithreadingu!
Teoretycznie tysiące, ale praktycznie każdy wątek zajmuje ~1MB pamięci. Większość aplikacji używa od kilku do kilkudziesięciu wątków. Dla większej liczby zadań używa się thread pools.
Nie! Na komputerze z jednym rdzeniem wątki będą się przełączać, co może być wolniejsze. Przyspieszenie jest widoczne przy operacjach I/O lub na maszynach wielordzeniowych.
To sytuacja gdy wynik programu zależy od kolejności wykonania wątków. Na przykład dwa wątki modyfikują tę samą zmienną - wynik może być nieprzewidywalny. Rozwiązanie: synchronizacja.
Problemy z multithreadingiem są trudne do debugowania bo mogą występować sporadycznie. Używaj logowania z nazwami wątków, narzędzi jak jstack, i testuj pod obciążeniem.
Sytuacja gdy dwa wątki czekają na siebie nawzajem i żaden nie może kontynuować. Na przykład: wątek A czeka na zasób B, wątek B czeka na zasób A. Program "zamarza".
Preferuj Runnable. Jest bardziej elastyczne (możesz dziedziczyć po innej klasie) i lepiej separuje logikę od mechanizmu wątku. Thread używaj tylko gdy naprawdę potrzebujesz rozszerzyć jego funkcjonalność.
🚀 Zadanie dla Ciebie
Stwórz program który symuluje pobieranie 5 plików z internetu. Każdy "download" ma trwać losowo od 1 do 3 sekund (użyj Thread.sleep()). Porównaj czas wykonania gdy robisz to sekwencyjnie vs. równolegle. Dodaj licznik który pokazuje ile downloadów się już skończyło - pamiętaj o synchronizacji!
Przydatne zasoby:
Jakie są Twoje doświadczenia z multithreadingiem w Javie? Z jakimi problemami się spotkałeś? Podziel się w komentarzach!