Wątki
Uruchamianie wątków
Obiekty pozwalają podzielić program na niezależne części. Możliwość pracy wielu wątków pozwala podzielić program na niezależnie działające zadania. Wątki są często nazywane procesami lekkimi. Każdy wątek, podobnie jak proces działa tak, jakby miał procesor do własnej dyspozycji. Procesor jest przydzielany kolejnym wątkom tak jak jest przydzielany kolejnym procesom.
Proces to wykonujący się program z własną przestrzenią adresową
Wątek to część pojedynczego procesu
Proces może posiadać wiele jednocześnie działających wątków
Często wątki wiążą pewną część programu z konkretnym zdarzeniem lub zasobem, np: w naszym interfejsie graficznym mamy przycisk start, uruchamiający obsługę bazy danych, chcemy by przycisk reagował na akcję ale nie chcemy być zmuszeni do badania przycisku w każdym fragmencie pisanego kodu. Żądamy, by przycisk reagował tak jakby był sprawdzany regularnie.
Wielowątkowość jest jedyną metodą uzyskania wielozadaniowości w Javie. Java ma wbudowaną wielowątkowość.
W środowisku wielozadaniowym bazującym na wątkach najmniejszą przetwarzaną jednostką kodu jest wątek. Oznacza to, że program wykonuje kilka różnych zadań.
Komunikacja międzyprosesowa jest kosztowna. Przełączanie kontekstu z między procesami ciężkimi jest drogie
Wątki zmniejszają koszt komunikacji międzyzadaniowej. Przełączanie kontekstu jest również tańsze.
Systemy jednowątkowe realizują podejście nazywane pętlą zdarzeń z odpytywaniem.
Wolny czas jednego wątku, np.: spowodowany oczekiwaniem dane z sieci, może być wykorzystany przez inną część programu.
Wielowątkowość zapewnia np.: odtwarzanie animacji z prędkością jednej klatki na sekundę, bez spowalniania pozostałych wątków. Blokady dotyczą jednego wątku.
Wątek może być w kilku stanach: wątek może działać, wątek może oczekiwać na wykonanie. Działający wątek można zawiesić, zawieszony wątek można wznowić - rozpocząć jego wykonanie od miejsca wcześniejszego zatrzymania.
Wątek może być zablokowany, gdy czeka na zasoby.
Wątek można przerwać, przerwanego wątku nie można wznowić.
W Javie, każdemu wątkowi może być przypisany priorytet. System stosuje priorytety do wybierania wątku, który ma dostać czas procesora po zatrzymaniu aktualnie wykonywanego wątku. Wątek może dobrowolnie zrzec się czasu procesora - wybierany jest wątek o najwyższym priorytecie.
Wątek może zostać wywłaszczony przez wątek o wyższym priorytecie. Wątek o niższym priorytecie, który dobrowolnie nie ustąpił jest wywłaszczany przez wątek o wyższym priorytecie, jest to wielozadaniowość z wywłaszczaniem.
W przypadku, gdy dwa wątki mają ten sam priorytet, o przydziale CPU decyduje system operacyjne. Wykonanie wielowątkowego programu z wątkami o tym samym priorytecie może być różne w różnych systemach operacyjnych. Dobrą praktyką jest takie planowanie wątków tak, by same oddawały sterowanie.
Konieczna jest synchronizacja wątków, np. gdy dwa wątki mają się ze sobą komunikować lub dzielić pewną strukturę danych. Znanym sposobem np. z C jest model monitora - gdy wątek wchodzi do monitora, wszystkie pozostałe wątki muszą czekać. W ten sposób monitor zabezpiecza dane współdzielone.
W Javie nie jest tworzony obiekt monitora, każdy obiekt posiada swój niejawny monitor, monitor ten jest uruchamiany automatycznie, gdy zostanie wywołana jedna z metod synchronizowanych danego obiektu. Jeżeli wątek wejdzie do metody synchronizowanej, pozostałe wątki nie mogą wykonać żadnej metody synchronizowanej.
Wątki mogą być budowane przez dziedziczenie z klasy Thread lub przez implementację interfejsu Runnable.
Klasa Thread dostarcza metody konieczne do obsługiwania wątków.
Wybrane metody
start() - rozpoczyna nowy wątek,
run() - zawiera algorytm wykonywany w wątku
sleep()
run() zawiera kod realizowany w wątku (często zawiera pętlę, której wykonywanie jest przerywane przez zachodzące zdarzenie)
getName(), setName() pobiera, ustawia nazwę wątku
getPriority, setPriority() pobiera, ustawia priorytet wątku
isAlive()sprawdza czy wątek nadal działa
join() czeka na zakończenie wątku
Przejdziemy do omówienia przykładów. Należy pamiętać, że pierwszy wątek powinien zakończyć swoje działanie po zakończeniu wszystkich wątków. W przykładzie, efekt ten został osiągnięty przez zatrzymanie wątku głównego za pomocą metody sleep()
P40Thread.java
W przykładzie powyższym, pętla z metody run() jest przerywana po 6 przejściach. W metodzie main() tworzone są 4 wątki. Tworzony jest obiekt dziedziczący po klasie Thread i wykonywana jest metoda start() tej klasy. Jeden z możliwych wyników, które otrzymamy podczas wykonywania tego programu zamieszczony jest poniżej.
Tworzenie 1 Watek 1(5) Tworzenie 2 Watek 2(5) Tworzenie 3 Tworzenie 4 Watek 3(5) Watek 4(5) Wątki zostały uruchomione nrWatku = 1 k= 0 nrWatku = 2 k= 0 nrWatku = 1 k= 1 nrWatku = 3 k= 0 nrWatku = 1 k= 2 nrWatku = 2 k= 1 nrWatku = 4 k= 0 nrWatku = 1 k= 3 nrWatku = 1 k= 4 nrWatku = 3 k= 1 nrWatku = 2 k= 2 nrWatku = 1 k= 5 Watek 1(4) nrWatku = 1 k= 0 nrWatku = 4 k= 1 nrWatku = 2 k= 3 nrWatku = 1 k= 1 nrWatku = 3 k= 2 nrWatku = 1 k= 2 nrWatku = 2 k= 4 nrWatku = 1 k= 3 nrWatku = 4 k= 2 nrWatku = 3 k= 3 nrWatku = 2 k= 5 Watek 2(4) nrWatku = 1 k= 5 Watek 1(3) nrWatku = 1 k= 0 nrWatku = 2 k= 0 nrWatku = 1 k= 1 nrWatku = 3 k= 4 nrWatku = 1 k= 2 nrWatku = 4 k= 3 nrWatku = 2 k= 1 nrWatku = 1 k= 3 nrWatku = 1 k= 4 nrWatku = 3 k= 5 |
Watek 3(4) nrWatku = 2 k= 2 nrWatku = 1 k= 5 Watek 1(2) nrWatku = 1 k= 0 nrWatku = 4 k= 4 nrWatku = 2 k= 3 nrWatku = 1 k= 1 nrWatku = 3 k= 0 nrWatku = 1 k= 2 nrWatku = 2 k= 4 nrWatku = 1 k= 3 nrWatku = 1 k= 4 Watek 4(4) nrWatku = 3 k= 1 nrWatku = 2 k= 5 Watek 2(3) nrWatku = 1 k= 5 Watek 1(1) nrWatku = 1 k= 0 nrWatku = 2 k= 0 nrWatku = 1 k= 1 nrWatku = 3 k= 2 nrWatku = 1 k= 2 nrWatku = 4 k= 0 nrWatku = 2 k= 1 nrWatku = 1 k= 3 nrWatku = 1 k= 4 nrWatku = 3 k= 3 nrWatku = 2 k= 2 nrWatku = 1 k= 5 Koniec wątku 1 nrWatku = 4 k= 1 nrWatku = 2 k= 3 nrWatku = 3 k= 4 nrWatku = 2 k= 4 nrWatku = 4 k= 2 nrWatku = 3 k= 5 Watek 3(3) nrWatku = 2 k= 5 Watek 2(2) nrWatku = 2 k= 0 nrWatku = 4 k= 3 nrWatku = 2 k= 1 nrWatku = 3 k= 1 nrWatku = 2 k= 2 |
nrWatku = 4 k= 4 nrWatku = 2 k= 3 nrWatku = 2 k= 4 nrWatku = 4 k= 5 Watek 4(3) nrWatku = 3 k= 3 nrWatku = 2 k= 5 Watek 2(1) nrWatku = 2 k= 0 nrWatku = 3 k= 4 nrWatku = 4 k= 0 nrWatku = 2 k= 1 nrWatku = 3 k= 5 Watek 3(2) nrWatku = 2 k= 2 nrWatku = 4 k= 1 nrWatku = 2 k= 3 nrWatku = 3 k= 0 nrWatku = 2 k= 4 nrWatku = 4 k= 2 nrWatku = 3 k= 1 nrWatku = 2 k= 5 Koniec wątku 2 nrWatku = 3 k= 2 nrWatku = 3 k= 3 nrWatku = 4 k= 4 nrWatku = 3 k= 4 nrWatku = 4 k= 5 Watek 4(2) nrWatku = 3 k= 5 Watek 3(1) nrWatku = 3 k= 0 nrWatku = 4 k= 0 nrWatku = 3 k= 1 nrWatku = 4 k= 1 nrWatku = 3 k= 2 nrWatku = 4 k= 2 nrWatku = 3 k= 3 nrWatku = 3 k= 4 nrWatku = 4 k= 3 nrWatku = 3 k= 5 Koniec wątku 3 nrWatku = 4 k= 4 nrWatku = 4 k= 5 Watek 4(1) nrWatku = 4 k= 0 |
nrWatku = 4 k= 1 nrWatku = 4 k= 2 nrWatku = 4 k= 3 nrWatku = 4 k= 4 nrWatku = 4 k= 5 Koniec wątku 4 Koniec wątku głównego |
---|
Każde wykonanie tego programu może dać inny wynik, jest to pewna niedogodność dla programistów przy testowaniu.
Klasy wewnętrzne i wątki
W następnym przykładzie stworzone są dwie wewnętrzne klasy wątku. Klasy wewnętrzne mogą działać na zmiennych klasy zewnętrznej(otaczającej). Zarówno statycznych jak i niestatycznych. Klasy wewnętrzne nie mogą posiadać zmiennych statycznych. Pierwszy z wątków jest uruchamiany jeden raz drugi jest uruchamiany dwukrotnie. Wyniki działania są umieszczone poniżej. A oto ten przykład.
P41Thread.java
A 1 pozostalo 5 s = 0 Wątki zostaly uruchomione B 2 pozostalo 3 s = 1 B 3 pozostalo 3 s = 2 A 1 k = 0 B 2 k = 0 Sat May 18 09:02:45 CEST 2013 A 1 k = 1 A 1 pozostalo 4 s = 3 B 3 k = 0 Sat May 18 09:02:50 CEST 2013 A 1 k = 0 B 2 k = 1 Sat May 18 09:02:55 CEST 2013 A 1 k = 1 A 1 k = 0 B 3 k = 1 Sat May 18 09:03:05 CEST 2013 B 2 k = 2 Sat May 18 09:03:05 CEST 2013 B 2 pozostalo 2 s = 5 A 1 pozostalo 2 s = 6 B 2 k = 0 Sat May 18 09:03:15 CEST 2013 A 1 k = 0 B 3 pozostalo 2 s = 7 A 1 k = 1 |
A 1 pozostalo 1 s = 8 B 2 k = 1 Sat May 18 09:03:25 CEST 2013 A 1 k = 0 A 1 k = 1 Koniec A 1 B 3 k = 0 Sat May 18 09:03:35 CEST 2013 B 2 k = 2 Sat May 18 09:03:35 CEST 2013 B 2 pozostalo 1 s = 9 B 3 k = 1 Sat May 18 09:03:50 CEST 2013 B 2 k = 1 Sat May 18 09:03:55 CEST 2013 B 3 k = 2 Sat May 18 09:04:05 CEST 2013 B 3 pozostalo 1 s = 10 B 2 k = 2 Sat May 18 09:04:05 CEST 2013 Koniec B 2 B 3 k = 0 Sat May 18 09:04:20 CEST 2013 B 3 k = 1 Sat May 18 09:04:35 CEST 2013 B 3 k = 2 Sat May 18 09:04:50 CEST 2013 Koniec B 3 Koniec wątku głównego |
---|
Interfejs Runnable
Następny przykład jest analogiczny do pierwszego spośród przykładów dotyczących wątków. W tym przykładzie wątki są zbudowane przez implementację interfesu Runnable.
P42Thread.java
Tworzenie 1 Tworzenie 2 Watek 1(5) Tworzenie 3 Tworzenie 4 Watek 3(5) Wątki zostały uruchomione Watek 2(5) Watek 4(5) nrWatku = 1 k= 0 nrWatku = 1 k= 1 nrWatku = 2 k= 0 nrWatku = 1 k= 2 nrWatku = 3 k= 0 nrWatku = 1 k= 3 nrWatku = 4 k= 0 nrWatku = 2 k= 1 nrWatku = 1 k= 4 nrWatku = 1 k= 5 nrWatku = 3 k= 1 nrWatku = 2 k= 2 nrWatku = 1 k= 0 nrWatku = 1 k= 1 nrWatku = 4 k= 1 |
nrWatku = 1 k= 2 nrWatku = 3 k= 2 nrWatku = 1 k= 3 nrWatku = 2 k= 4 nrWatku = 1 k= 4 nrWatku = 1 k= 5 nrWatku = 3 k= 3 nrWatku = 4 k= 2 nrWatku = 2 k= 5 Watek 2(4) nrWatku = 1 k= 0 nrWatku = 1 k= 1 nrWatku = 2 k= 0 nrWatku = 1 k= 2 nrWatku = 3 k= 4 nrWatku = 1 k= 3 nrWatku = 4 k= 3 nrWatku = 2 k= 1 nrWatku = 1 k= 4 nrWatku = 1 k= 5 Watek 1(2) nrWatku = 3 k= 5 Watek 3(4) nrWatku = 1 k= 0 nrWatku = 1 k= 1 |
nrWatku = 4 k= 4 nrWatku = 2 k= 3 nrWatku = 1 k= 2 nrWatku = 3 k= 0 nrWatku = 1 k= 3 nrWatku = 1 k= 4 nrWatku = 1 k= 5 Watek 1(1) nrWatku = 3 k= 1 nrWatku = 4 k= 5 Watek 4(4) nrWatku = 2 k= 5 Watek 2(3) nrWatku = 1 k= 0 nrWatku = 1 k= 1 nrWatku = 2 k= 0 nrWatku = 1 k= 2 nrWatku = 3 k= 2 nrWatku = 1 k= 3 nrWatku = 4 k= 0 nrWatku = 2 k= 1 nrWatku = 1 k= 4 nrWatku = 1 k= 5 nrWatku = 3 k= 3 nrWatku = 2 k= 2 nrWatku = 4 k= 1 |
nrWatku = 2 k= 3 nrWatku = 3 k= 4 nrWatku = 2 k= 4 nrWatku = 3 k= 5 Watek 3(3) nrWatku = 2 k= 5 Watek 2(2) nrWatku = 2 k= 0 nrWatku = 3 k= 0 nrWatku = 4 k= 3 nrWatku = 2 k= 1 nrWatku = 3 k= 1 nrWatku = 2 k= 2 nrWatku = 4 k= 4 nrWatku = 2 k= 3 nrWatku = 3 k= 2 nrWatku = 2 k= 4 nrWatku = 3 k= 3 nrWatku = 4 k= 5 Watek 4(3) nrWatku = 2 k= 5 Watek 2(1) nrWatku = 2 k= 0 nrWatku = 3 k= 4 nrWatku = 4 k= 0 nrWatku = 2 k= 1 |
nrWatku = 3 k= 5 Watek 3(2) nrWatku = 2 k= 2 nrWatku = 4 k= 1 nrWatku = 2 k= 3 nrWatku = 3 k= 0 nrWatku = 2 k= 4 nrWatku = 3 k= 1 nrWatku = 4 k= 2 nrWatku = 2 k= 5 nrWatku = 3 k= 2 nrWatku = 4 k= 3 nrWatku = 3 k= 3 nrWatku = 4 k= 4 nrWatku = 3 k= 4 nrWatku = 3 k= 5 Watek 3(1) nrWatku = 4 k= 5 Watek 4(2) nrWatku = 3 k= 0 nrWatku = 4 k= 0 nrWatku = 3 k= 1 nrWatku = 4 k= 1 nrWatku = 3 k= 2 nrWatku = 3 k= 3 nrWatku = 4 k= 2 nrWatku = 3 k= 4 |
---|
nrWatku = 4 k= 3 nrWatku = 3 k= 5 nrWatku = 4 k= 4 nrWatku = 4 k= 5 Watek 4(1) nrWatku = 4 k= 1 nrWatku = 4 k= 2 nrWatku = 4 k= 3 nrWatku = 4 k= 4 nrWatku = 4 k= 5 Koniec wątku głównego |
---|
Wykorzystanie metod klasy Thread
W przykładzie następnym oczekiwanie na zamknięcie wszystkich wątków jest wykonane przez wywołanie metody join() dla każdego z wątków.
P43Thread.java
Priorytety
W następnym przykładzie prześledzimy działanie priorytetów. Program ten może różnie działać, w różnych systemach operacyjnych. Jest to zależne od sposobu traktowania wątków. Mimo ustawienia priorytetów może się zdarzyć, że jeden z wątków zawłaszczy procesor. Dobrą metodą na uzyskanie płynności pracy wątków jest dobrowolne oddawanie sterowania do procesora. Program uruchamia 3 wątki, główny i dwa potomne. Wątki potomne powiększają wartość jednej zmiennej o jeden. Wątek główny jest usypiany na pewien czas, tak więc dwa wątki potomne rywalizują wyłącznie ze sobą i pozostałymi zadaniami systemu (na to nie mamy wpływu). Wyniki uzyskiwane świadczą o tym, że w tym systemie, następuje przełączanie kontekstu bez konieczności zaprogramowania oddawania sterowania do procesora. Słowo kluczowe volatile powoduje, że pętla ze zmienną petla nie będzie optymalizowana.
P44Thread.java
Priorytet może się zmieniać (obecnie) od 1-10, czyli od Thread.MIN_PRIORITY, przez Thread.NORM_PRIORITY do Thread.MAX_PRIORITY. Wyniki uzyskane w tym programie świadczą o tym, że wątki są wykonywane w sposób płynny, jeśli czas przydzielany poszczególnym watkom jest przyznawany w odpowiednio długi czasie.
Synchronizacja wątków
Wątki mogą korzystać ze wspólnych danych, często wymaga to synchronizacji. W Javie synchronizacja jest dostępna na poziomie języka. Podstawowym pojęciem związanym z synchronizacją jest monitor (semafor). Monitor to obiekt, który zakłada blokadę wzajemnie wykluczającą. Gdy wątek założy blokadę (wchodzi do monitora), wszystkie inne wątki, które chcą wejść do monitora będą musiały czekać. Synchronizacja w Javie sprowadza się do wywołania metody z modyfikatorem synchronized. Jeśli jeden wątek zacznie wykonywać metodę synchronizowaną wtedy pozostałe wątki, które chcą wykonać tę lub inną metodę synchronizowaną dla tego samego obiektu muszą czekać. Przyjrzyjmy się programowi, który wyprowadza na ekran tekst w nawiasach. W poniższym programie metoda drukuj() wykonywana jest w metodzie run() i uruchamiane są trzy wątki. W metodzie drukuj() oddawane jest sterowanie do procesora i wydruki z różnych wątków przeplatają się.
P45Thread.java
Metoda drukująca powinna być metodą synchronizowaną.
P46Thread.java
Synchronizowanie metody drukuj() z klasy DrukA jest możliwe, gdy mamy dostęp do źródła klasy zawierającej. Jeśli jest to niemożliwe, to można zapewnić synchronizowany dostęp do obiektu tej klasy. Program może mieć poniższą postać.
P47Thread.java
Komunikacja międzywątkowa
Synchronizowane metody blokują dostęp dla innych wątków w sposób bezwzględny. Klasyczny problem konsument - producent, przy założeniu, że konsument pobiera dane gdy producent je wyprodukuje, i na odwrót. Do realizacji tego zadania potrzebne będą metody służące komunikacji międzywątkowej. Metody te pochodzą z klasy Object i są to metody:
wait() - wywołujący ją wątek zostaje uśpiony do momentu, gdy inny wątek nie po wejściu do monitora nie wywoła metody notify().
notify() - budzi wątek, który wywołał metodę wait()
notifyAll() - budzi wszystkie wątki, które wywołały metodę wait()
Poniższy program realizuje zadanie producent - konsument. Program zapisany jest za pomocą czterech. Klasa Kolejka będzie zawierała metody pozwalające na ustawienie wyprodukowanej liczby (produkt) i jej pobranie, metody te muszą być zsynchronizowane. Ponadto należy zadbać o to, by żadna wyprodukowana liczba nie była pobrana wielokrotnie i by wyprodukowana liczba nie była pominięta.
Klasy Producet i Konsument będą klasami budującymi wątki korzystające Wcześniej zbudowane metody z klasy Kolejka. Konsument będzie pobierał z kolejki, producent będzie wstawiał do kolejki. Jeśli zostaną usunięte linie z komentarzami umieszczonymi po instrukcjach, wtedy nie uzyskamy, mimo synchronizacji tego samego efektu. Będzie możliwe produkowanie wielu liczb bez pobierania i na odwrót.
P48Thread.java
Istnieje możliwość blokady wzajemnej. Wystarczy sobie wyobrazić wątek X wchodzący do monitora obiektu X i inny wątek wchodzący do monitora obiektu Y. Wątek z obiektu X próbuje wykonać metodę synchronizowaną obiektu Y, wątek ten zostanie zablokowany. Wątek z obiektu Y spróbuje wykonać dowolną metodę synchronizowaną obiektu X, czas oczekiwania na wykonanie tej metody będzie nieskończony. Dostanie się do obiektu X wymaga zwolnienia blokady z Y.
Wzajemna blokada jest błędem bardzo trudnym do wykrycia. Wzajemna blokada jest dość rzadko spotykana, czas wykonania dwóch lub więcej wątków muszą ułożyć się w odpowiedni sposób. Blokada wzajemna może dotyczyć więcej niż dwóch wątków i synchronizowanych obiektów. Poniższy przykład pokazuje wzajemną blokadę wątków. Metody metodaRazB() i metodaDwaB() nie mają szansy na wykonanie.
P49Thread.java
Zawieszanie wątków, wznawianie wątków, zatrzymywanie wątków
We wczesnych wersjach Javy do sterowania wątkami, a więc do zawieszania wznawiania i zatrzymywania wątków służyły metody: suspend(), resume(), stop(). Metoda suspend() powodowała czasem błędy systemowe. Zdarzało się, że wątek dostawał blokadę na dostęp do danych, zatrzymanie takiego wątku nie zwalniało blokady i pozostałe wątki oczekujące na dostęp do danych mogły doprowadzić blokady wzajemnej. Metody (resume() bez suspend() nie ma sensu) te obecnie uważane są za przestarzałe, wręcz nie należy ich używać.
Obecnie do tego typu sterowania używane są odpowiednie flagi, których wartości są ustawiane na wartość true lub false i mogą być warunkiem wywołania metody wait() lub notify().
Ten sam tryb można zastosować do zatrzymywania wątków
Ilustracją takiego postępowania jest poniższy program. Mimo, że takie wstrzymywanie i wznawianie wątków wydaje się być mniej eleganckie niż użycie metod suspend(), resume(), należy stosować właśnie takie podejście.
P50Thread.java