Problemy, które wielowątkowość rozwiązuje w Javie
Zasadniczo wielowątkowość Java została wynaleziona, aby rozwiązać dwa główne problemy:-
Wykonuj wiele czynności jednocześnie.
W powyższym przykładzie różne wątki (czyli członkowie rodziny) wykonywali kilka czynności równolegle: umyli naczynia, poszli do sklepu, złożyli rzeczy.
Można podać przykład bardziej „programistyczny”. Wyobraź sobie, że masz program z interfejsem użytkownika. Po kliknięciu przycisku Kontynuuj w programie powinny nastąpić pewne obliczenia, a użytkownikowi powinien wyświetlić się następujący ekran interfejsu. Jeśli te czynności będą wykonywane sekwencyjnie, po kliknięciu przycisku „Kontynuuj” program po prostu się zawiesi. Użytkownik zobaczy ten sam ekran z przyciskiem „Kontynuuj”, aż do zakończenia wszystkich wewnętrznych obliczeń i dotarcia programu do części, w której rozpocznie się rysowanie interfejsu.
Cóż, poczekajmy kilka minut!
Możemy także przerobić nasz program lub, jak mówią programiści, „równolegle”. Niech niezbędne obliczenia zostaną wykonane w jednym wątku, a renderowanie interfejsu w innym. Większość komputerów ma do tego wystarczające zasoby. W tym przypadku program nie będzie „głupi”, a użytkownik będzie spokojnie poruszał się pomiędzy ekranami interfejsu, nie przejmując się tym, co dzieje się w środku. Nie przeszkadza :)
-
Przyspiesz obliczenia.
Tutaj wszystko jest znacznie prostsze. Jeśli nasz procesor ma kilka rdzeni, a większość procesorów jest teraz wielordzeniowych, naszą listę zadań można rozwiązać równolegle przez kilka rdzeni. Oczywiście, jeśli musimy rozwiązać 1000 problemów, a każdy z nich zostanie rozwiązany w ciągu sekundy, jeden rdzeń poradzi sobie z listą w 1000 sekund, dwa rdzenie w 500 sekund, trzy w nieco ponad 333 sekundy i tak dalej.
Thread
. Oznacza to, że aby utworzyć i uruchomić 10 wątków, będziesz potrzebować 10 obiektów tej klasy. Napiszmy najprostszy przykład:
public class MyFirstThread extends Thread {
@Override
public void run() {
System.out.println("I'm Thread! My name is " + getName());
}
}
Aby tworzyć i uruchamiać wątki, musimy utworzyć klasę i dziedziczyć ją z java.lang
. Thread
i zastąp znajdującą się w nim metodę run()
. To ostatnie jest bardzo ważne. To w metodzie run()
określamy logikę, którą musi wykonać nasz wątek. Teraz, jeśli utworzymy instancję MyFirstThread
i ją uruchomimy, metoda run()
wypisze do konsoli linię z jej nazwą: metoda getName()
wypisuje „systemową” nazwę wątku, która jest przydzielana automatycznie. Chociaż właściwie, dlaczego „jeśli”? Twórzmy i testujmy!
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
MyFirstThread thread = new MyFirstThread();
thread.start();
}
}
}
Dane wyjściowe konsoli: Jestem wątkiem! Nazywam się Thread-2. Jestem Thread! Nazywam się Thread-1. Jestem Thread! Nazywam się Thread-0. Jestem Thread! Nazywam się Thread-3. Jestem Thread! Nazywam się Thread-6. Jestem Thread! Nazywam się Thread-7. Jestem Thread! Nazywam się Thread-4. Jestem Thread! Nazywam się Thread-5. Jestem Thread! Nazywam się Thread-9. Jestem Thread! Nazywam się Thread-8. Tworzymy 10 wątków (obiektów) MyFirstThread
, które dziedziczą Thread
i uruchamiają je, wywołując metodę obiektu start()
. Po wywołaniu metody , start()
jej metoda zaczyna działać run()
i wykonywana jest logika w niej zapisana. Uwaga: nazwy wątków nie są uporządkowane. To dość dziwne, dlaczego nie wykonano ich po kolei: Thread-0
, Thread-1
, Thread-2
i tak dalej? To jest dokładnie przykład sytuacji, w której standardowe, „sekwencyjne” myślenie nie zadziała. Fakt jest taki, że w tym przypadku wydajemy jedynie polecenia utworzenia i uruchomienia 10 wątków. O kolejności ich uruchamiania decyduje harmonogram wątków: specjalny mechanizm wewnątrz systemu operacyjnego. To, jak dokładnie jest ona zorganizowana i na jakich zasadach podejmuje decyzje, jest bardzo złożonym tematem i nie będziemy się teraz w to zagłębiać. Najważniejszą rzeczą do zapamiętania jest to, że programista nie może kontrolować kolejności wykonywania wątków. Aby zdać sobie sprawę z powagi sytuacji, spróbuj main()
jeszcze kilka razy uruchomić metodę z powyższego przykładu. Drugie wyjście konsoli: Jestem wątkiem! Nazywam się Thread-0. Jestem Thread! Nazywam się Thread-4. Jestem Thread! Nazywam się Thread-3. Jestem Thread! Nazywam się Thread-2. Jestem Thread! Nazywam się Thread-1. Jestem Thread! Nazywam się Thread-5. Jestem Thread! Nazywam się Thread-6. Jestem Thread! Nazywam się Thread-8. Jestem Thread! Nazywam się Thread-9. Jestem Thread! Nazywam się Thread-7. Trzecie wyjście konsoli: Jestem Thread! Nazywam się Thread-0. Jestem Thread! Nazywam się Thread-3. Jestem Thread! Nazywam się Thread-1. Jestem Thread! Nazywam się Thread-2. Jestem Thread! Nazywam się Thread-6. Jestem Thread! Nazywam się Thread-4. Jestem Thread! Nazywam się Thread-9. Jestem Thread! Nazywam się Thread-5. Jestem Thread! Nazywam się Thread-7. Jestem Thread! Nazywam się Thread-8
Problemy stwarzane przez wielowątkowość
Na przykładzie z książkami widziałeś, że wielowątkowość rozwiązuje dość istotne problemy, a jej zastosowanie przyspiesza pracę naszych programów. W wielu przypadkach – wielokrotnie. Ale nie bez powodu wielowątkowość jest uważana za złożony temat. W końcu, jeśli jest niewłaściwie używany, zamiast go rozwiązywać, stwarza problemy. Kiedy mówię „tworzyć problemy”, nie mam na myśli czegoś abstrakcyjnego. Istnieją dwa specyficzne problemy, które może powodować wielowątkowość: zakleszczenie i sytuacja wyścigowa. Zakleszczenie to sytuacja, w której wiele wątków oczekuje na zajęte przez siebie zasoby i żaden z nich nie może kontynuować wykonywania. Porozmawiamy o tym więcej w przyszłych wykładach, ale na razie wystarczy ten przykład: wyobraźmy sobie, że wątek-1 współpracuje z jakimś Obiektem-1, a wątek-2 pracuje z Obiektem-2. Program jest napisany w następujący sposób:- Wątek-1 przestanie działać z Obiektem-1 i przełączy się na Obiekt-2, gdy tylko Wątek-2 przestanie działać z Obiektem 2 i przełączy się na Obiekt-1.
- Wątek-2 przestanie działać z Obiektem-2 i przełączy się na Obiekt-1, gdy tylko Wątek-1 przestanie działać z Obiektem 1 i przełączy się na Obiekt-2.
public class MyFirstThread extends Thread {
@Override
public void run() {
System.out.println("Выполнен поток " + getName());
}
}
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
MyFirstThread thread = new MyFirstThread();
thread.start();
}
}
}
A teraz wyobraź sobie, że program odpowiada za obsługę robota przygotowującego jedzenie! Thread-0 wyjmuje jajka z lodówki. Strumień 1 włącza piec. Stream-2 wyjmuje patelnię i kładzie ją na kuchence. Stream 3 rozpala ogień w piecu. Strumień 4 wlewa olej na patelnię. Strumień 5 rozbija jajka i wlewa je na patelnię. Strumień 6 wrzuca naboje do kosza na śmieci. Stream-7 usuwa gotową jajecznicę z ognia. Potok-8 nakłada na talerz jajecznicę. Strumień 9 zmywa naczynia. Spójrz na wyniki naszego programu: Wątek-0 wykonany Wątek-2 wykonany Wątek-1 wątek wykonany Wątek-4 wykonany Wątek-9 wykonany Wątek-5 wykonany Wątek-8 wykonany Wątek-7 wykonany wątek Wątek-7 wątek wykonany -3 Wykonany wątek 6. Czy skrypt jest fajny? :) A wszystko dlatego, że działanie naszego programu uzależnione jest od kolejności wykonywania wątków. Przy najmniejszym naruszeniu sekwencji nasza kuchnia zamienia się w piekło, a oszalały robot niszczy wszystko wokół. Jest to również częsty problem w programowaniu wielowątkowym, o którym usłyszysz nie raz. Na koniec wykładu chciałbym polecić Państwu książkę o wielowątkowości.
GO TO FULL VERSION