WspółbieŜność
Transkrypt
WspółbieŜność
WspółbieŜność
W programowaniu sekwencyjnym, kaŜdy program ma początek, sekwencje instrukcji do
wykonania i koniec. W kaŜdym momencie działania programu moŜemy wskazać miejsce, w
którym znajduje się sterowanie. Taki program stanowi zatem pojedynczy, sekwencyjny
przepływ sterowania. Program moŜe jednak składać się z wielu przepływów sterowania,
zwanych wątkami (ang. thread).
KaŜdy wątek ma początek, sekwencje instrukcji i koniec. Wątek nie jest niezaleŜnym
programem, jest wykonywany jako część programu. W programie wiele wątków moŜe być
wykonywanych jednocześnie i kaŜdy z nich moŜe wykonywać w tym samym czasie
odmienne zadania (ang. tasks).
Jeśli program napisany wielowątkowo (ang. multithreaded), wykonywany jest na maszynie
wieloprocesorowej, to róŜne wątki mogą być wykonywane w tym samym czasie na róŜnych
procesorach. Sterowanie programu w takich przypadkach przebiega współbieŜnie (ang.
concurrent). Na komputerach jednoprocesorowych wykonanie programów wielowątkowych
jest tylko emulowane. Emulacja ta polega na naprzemiennym przydzielaniu czasu procesora
poszczególnym wątkom wg. pewnego algorytmu (zaimplementowanego w systemie
operacyjnym). To, w jakim stopniu wątek będzie mógł wykorzystywać procesor, zaleŜy od
priorytetu wątku (priorytety zostaną omówione dalej w tym rozdziale).
Tak jak w programie sekwencyjnym, kaŜdy wątek ma swoje zarezerwowane zasoby (jak np.
licznik instrukcji), lecz oprócz tego moŜe korzystać z zasobów programu, w którym jest
wykonywany.
W Javie wątki są obiektami zdefiniowanymi za pomocą specjalnego rodzaju klas.
Program wielowątkowy definiujemy, na dwa sposoby:
•
•
jako podklasę klasy Thread;
implementując interfejs Runnable.
Jeśli zdefiniujemy klasę z moŜliwością pracy jako wątek nie oznacza to automatycznie, Ŝe
klasa ta będzie wykonana jako taki. Zostanie to wyjaśnione dalej w tym rozdziale.
Przykład prostego programu wielowątkowego
Spójrzmy jak wygląda wielowątkowość w praktyce.
MoŜemy zdefiniować naszą klasę jako wątek poprzez rozszerzenie klasy java.lang.Thread.
Ten sposób daje nam bezpośredni dostęp do wszystkich metod kontrolujących wątek
zdefiniowanych w klasie Thread.
W tej przykładowej aplikacji zdefiniowano dwie klasy: Watek i PierwszyWielowatkowy.
Klasa Watek definiuje pojedynczy wątek, który będzie wykonywany w aplikacji
PierwszyWielowatkowy, klasa ta dziedziczy z klasy Thread będącej częścią pakietu
java.lang.
Przykład 2.24 Rozszerzenie klasy Thread
class Watek extends Thread
{
String wysun = "";
public Watek(String str, int numer)
{
super(str);
// Ustawienie wcięcia z jakim będzie wyświetlana nazwa Watku
for (int i = 1; i < numer; i++) wysun = wysun + "\t";
}
public void run()
{
for (int i = 0; i < 4; i++)
{
System.out.println(wysun + i + " " + getName());
try
{ sleep( (int)(Math.random() * 1000) ); }
catch ( InterruptedException e )
{ e.printStackTrace(); }
}
System.out.println(wysun + getName()+ " koniec" );
}
}
Pierwszą metodą klasy Watek jest konstruktor z dwoma argumentami: nazwa wątku, typu
String i parametr określający wcięcie, z jakim będzie wyświetlana na ekranie nazwa wątku. W
konstruktorze tym pierwszą instrukcją jest wywołanie konstruktora nadklasy, który ustawia
nazwę wątku. Następnie w bloku for ustawiane jest wcięcie z jakim na ekranie
Następną metodą klasy Watek jest metoda run(). Metoda run jest najwaŜniejszą metodą
kaŜdego wątku, w której zdefiniowane są wszystkie działania wykonywane przez wątek.
Metoda run() klasy Watek zawiera pętlę for wykonywaną cztery razy. W kaŜdej iteracji
metoda ta wyświetla na ekranie numer iteracji i nazwę wątku z odpowiednim wcięciem
zaleŜnym od parametru numer konstruktora. Po wyświetleniu tego napisu wątek za pomocą
metody sleep() jest usypiany na przedział czasu wylosowany przez metodę random klasy
Math. Po wykonaniu wszystkich iteracji wyświetlana jest na ekranie nazwa wątku z napisem
"koniec".
Klasa PierwszyWielowatkowy definiuje aplikację. W metodzie main() tworzone są cztery
wątki o nazwach: Janek, Magda, Wacek i Ola (przypuśćmy, Ŝe kaŜda z tych osób ma
zadzwonić do czworga znajomych, kto pierwszy zdoła to zrobić, ten wygrywa). Druga
metoda tej klasy, znana nam i zdefiniowana juŜ w tej pracy, pauza() zatrzymuje wyniki pracy
aplikacji na ekranie.
class PierwszyWielowatkowy
{
public static void main (String[] args) throws Exception
{
new Watek("Janek",1).start();
new Watek("Magda",2).start();
new Watek("Wacek",3).start();
new Watek("Ola",4).start();
pauza(); }
static void pauza() throws Exception
{ /* ... Zdefiniowana juŜ wcześniej w tej pracy */ }
}
W metodzie main() wszystkie wątki zaraz po ich utworzeniu są uruchamiane dzięki uŜyciu
metody start().
Po skompilowaniu i uruchomieniu tej aplikacji efekt działania będzie podobny do
przedstawionego na ekranie.
Ilustracja 2-9 Przykładowy efekt wykonania aplikacji PierwszyWielowatkowy.
Widać, Ŝe wyniki działania wszystkich wątków są na ekranie przemieszane. Dzieje się tak
dlatego, Ŝe wszystkie wątki typu Watek działają jednocześnie. Wszystkie metody run()
wykonywane są jednocześnie i wyprowadzają na ekran efekty swojego działania w tym
samym czasie. Prócz czterech wątków utworzonych w metodzie main() wciąŜ działa wątek
główny aplikacji. Widzimy, Ŝe pierwszym napisem jaki wyprowadzono na ekran jest:
Nacisnij Enter....
który wyprowadza na ekran metoda pauza(). Jest tak mimo tego, Ŝe w kodzie metody main()
metoda pauza() jest na samym końcu. To pokazuje nam, Ŝe w chwili utworzenia nowych
wątków działają one niezaleŜnie od wątku głównego programu.
Ciało wątku
Wszystkie zadania, jakie ma wykonywać wątek umieszczone są w metodzie run wątku. Po
utworzeniu i inicjalizacji wątku, środowisko przetwarzania wywołuje metodę run.
W ciele metody run często pojawia się pętla. Na przykład, wątek odpowiedzialny za animację
w pętli w metodzie run moŜe wyświetlać serię obrazków. Niekiedy metoda run wątku
wykonuje operacje, które zajmują duŜo czasu np. ładowanie i odgrywanie dŹwięków lub
filmów.
Implementacja interfejsu Runnable
W wielu aplikacjach mamy do czynienia z koniecznością implementacji wielodziedziczenia,
np. chcemy zdefiniować klasę, która ma własności wątku i jednocześnie rozszerza
właściwości jakiejś innej klasy. PoniewaŜ w Javie nie jest moŜliwe wielodziedziczenie,
rozwiązaniem w takim przypadku jest implementacja interfejsu Runnable.
Definicja interfejsu Runnable przedstwia się następująco:
public interface java.lang.Runnable
{ // Metody
public abstract void run();
}
W rzeczywistości klasa Thread takŜe implementuje interfejs Runnable. Interfejs Runnable ma
tylko jedną metodę: run. Kiedy definiujemy klasę jako implementującą ten interfejs, musimy
zadeklarować metodę run. W metodzie run wykonywane są wszystkie zadania, które mają
być realizowane przez dany wątek.
Przykład klasy implementującej interfejs Runnable:
class MojaKlasa extends java.applet.Applet implements Runnable
{
public void run()
{
//ciało metody run
}
//... inne metody klasy MojaKlasa
}
Spójrzmy jak wygląda aplikacja, która wykonuje te same zadania, co przedstawiony
wcześniej program , i jednocześnie wykonywana jest wielowątkowo, implementując interfejs
Runnable.
Przykład 2.25 Implementacja interfejsu Runnable
Główne zmiany w porównaniu z wcześniejsza wersją zaznaczone są pogrubioną czcionką.
class WatekPodstawowy implements Runnable
{
String wysun = "";
// W polu danych biezacy przechowywana będzie referencja
// do wątku, w którym wykonana zostanie klasa WatekPodstawowy
Thread biezacy;
public WatekPodstawowy( int numer)
{
// metoda statyczna currentThread() klasy Thread zwaca
// referencję do bieŜącego wątku
biezacy = Thread.currentThread();
for (int i = 1; i < numer; i++)
wysun = wysun + "\t";
}
public void run()
{
for (int i = 0; i < 4; i++)
{
// dzięki referencji biezacy moŜemy na rzecz tego
// wątku wykonać metodę getName() (z klasy Thread)
System.out.println(wysun + i + " " + biezacy.getName());
try
{
biezacy.sleep((int)(Math.random() * 1000));
}
catch (InterruptedException e) {}
}
System.out.println(wysun + biezacy.getName()+ " koniec" );
}
}
Klasa DrugiWielowatkowy róŜni się od klasy PierwszyWielowatkowy o wiele bardziej niŜ
klasa WatekPodstawowy od jej poprzedniczki. W tamtej klasie tworzyliśmy nowe wątki i
uruchamialiśmy je. W tym przypadku mamy tablice wątków watki[], której elementy są
wątkami kontrolującymi wykonanie obiektów klasy WatekPodstawowy.
Przykład 2.26 Definicja klasy DrugiWielowatkowy
public class DrugiWielowatkowy
{
// deklaracja tablicy wątków, deklarujemy ją jako static,
// bowiem tylko do pól statycznych klasy moŜemy się
// odwołać w statycznej metodzie main()
static Thread watki[];
public static void main (String[] args) throws Exception
{
// przypisanie do pola danych watki tablicy referencji
// do obiektów typu Thread
watki = new Thread[4];
// inicjalizacja elementów tablicy watki,
// utworzenie nowych wątków
watki[0] = new Thread( new WatekPodstawowy(1),"Janek");
watki[1] = new Thread( new WatekPodstawowy(2),"Magda");
watki[2] = new Thread( new WatekPodstawowy(3),"Wacek");
watki[3] = new Thread( new WatekPodstawowy(4),"Ola");
// uruchomienie wątków
for (int i=0; i<4; i++)
watki[i].start();
pauza(); }
static void pauza() throws Exception
{ /* ... Zdefiniowana juŜ wcześniej w tej pracy */ }
}
Stany wątku
W czasie swego istnienia wątek moŜe znajdować się w jednym z kilku stanów.
PoniŜszy rysunek przedstawia stany, w jakich moŜe znajdować się wątek podczas swego
Ŝycia oraz metody, których wywołanie powoduje przejście wątku do następnego stanu.
Diagram ten nie jest kompletnym, skończonym diagramem stanów wątku ale obejmuje
najbardziej interesujące i najczęściej występujące stany, w jakich wątek moŜe się znaleŹć.
Rysunek 2-5 Stany wątków.
Omówmy teraz poszczególne stany:
•
nowy wątek
PoniŜsza instrukcja tworzy nowy wątek ale nie uruchamia go, lecz pozostawia wątek w stanie
"nowy wątek".
Thread mojWatek = new MojaKlasaWatku();
Po wykonaniu tej instrukcji mamy zaledwie pusty obiekt Thread. adne zasoby systemowe nie
zostały jeszcze alokowane dla tego wątku. Kiedy wątek znajduje się w tym stanie, moŜemy
jedynie wykonać metod. start, uruchamiającą wątek, lub stop, kończącą działania wątku.
Wszelkie próby wywołania innych metod dla wątków w tym stanie nie mają sensu i powodują
wystąpienie wyjątku IllegalThreadStateException.
•
wykonywany
Przeanalizujmy poniŜsze dwie linie kodu:
Thread mojWatek = new MojaKlasaWatku();
mojWatek.start();
Metoda start tworzy zasoby systemowe potrzebne do wykonania wątku, przygotowuje wątek
do uruchomienia, oraz woła metodę run. Od tego momentu wątek jest w stanie
"wykonywany". Nie oznacza to jednak automatycznie, Ŝe wątek zostaje uruchomiony. Wiele
komputerów ma tylko jeden procesor, co powoduje, Ŝe niemoŜliwe jest uruchomienie wielu
wątków w tym samym momencie. ?rodowisko przetwarzania Javy musi implementować
system przydziału czasu procesora (ang. scheduler), który dzieli czas procesora między
wszystkie wątki będące w stanie "wykonywany". Więcej informacji o przydzielaniu czasu
procesora wątkom znajduje się w rozdziale poświęconym priorytetom wątków.
•
nie wykonywany
Wątek przechodzi do stanu "nie wykonywany" gdy zachodzi jedno z poniŜszych zdarzeń:
•
•
•
•
wywołano jego metodę sleep,
wywołano jego metodę suspend,
wątek wykonuje swoją metodę wait,
wątek jest zablokowany przy operacji wejścia / wyjścia (ang. I/O).
Przykładowo, wykonanie metody sleep(10000) w poniŜszym kodzie powoduje uśpienie
bieŜącego wątku na 10 sekund (10 000 milisekund):
try
{ Thread.sleep(10000); }
catch (InterruptedException e)
{ }
W czasie tych 10 sekund gdy wątek jest uśpiony, nawet gdy procesor staje się dostępny dla
tego wątku, wątek ten nie zostaje uruchomiony. Po upływie 10 sekund wątek przechodzi do
stanu "wykonywany" i jeśli procesor jest dostępny, jest on uruchamiany. Dla kaŜdego
przypadku przejścia wątku do stanu "nie wykonywany" istnieją specyficzne warunki, jakie
muszą być spełnione, aby nastąpił powrót do stanu "wykonywany".
PoniŜej przedstawiono warunki, jakie muszą być spełnione, aby nastąpił powrót do stanu
"wykonywany":
•
•
•
•
•
jeśli wątek uśpiono (metoda sleep ), musi upłynąć określona liczba milisekund,
jeśli wątek zawieszono (metoda suspend), inny wątek musi wywołać metodę resume
wątku, powodującą jego odwieszenie,
jeśli wątek czeka na np. ustawienie jakiejś zmiennej, to obiekt, do którego naleŜy ta
zmienna, musi ją odstąpić a następnie wywołać metodę notify lub notifyAll,
jeśli wątek jest zablokowany przy operacjach wejścia / wyjścia, wtedy operacje te
muszą być zakończone.
zakończony
Wątek moŜe zakończyć działanie z dwu powodów: albo naturalnie zakończy swe działanie
albo zostanie zabity (ang. kill). Wątek naturalnie kończy swoje działanie wtedy, gdy jego
metoda run kończy się normalnie. W poniŜszym przykładzie pętla while wykonywana jest 50
razy i metoda run naturalnie kończy swoje działanie:
public void run()
{
int i = 1;
while (i < 51)
{
System.out.println( i + " iteracja");
i++;
}
}
MoŜemy takŜe zabić wątek w kaŜdym momencie poprzez wywołanie jego metody stop. W
poniŜszym przykładzie tworzony i uruchamiany jest wątek mojWatek, następnie bieŜący
wątek jest usypiany na 10 sekund. Kiedy bieŜący wątek się budzi, w przedostatniej linii
przykładu wątek mojWatek zostaje zabity:
Thread mojWatek = new MojaKlasaWatku();
mojWatek.start();
try
{
Thread.sleep(10000);
} catch (InterruptedException e){}
mojWatek.stop();
Metoda stop generuje w wątku obiekt (wyjątek) ThreadDeath, słuŜący do zabicia go. Oznacza
to, Ŝe wątek w takim przypadku zabijany jest asynchronicznie. Wątek zostaje zabity wtedy,
gdy rzeczywiście odbierze wyjątek ThreadDeath.
Metoda stop oznacza nagłe zakończenie wykonania metody run wątku. Jeśli metoda run
wykonuje jakieś waŜne obliczenia, metoda stop moŜe spowodować przerwanie wykonywania
programu w stanie niespójnym. Dlatego nie powinno się wołać metody stop wtedy, gdy
chcemy zakończyć wątek, lecz zrobić to w łagodniejszy sposób np. poprzez ustawienie flagi,
która informuje metodę run, Ŝe powinna zakończyć swoje wykonanie.
Wyjątek IllegalThreadStateException
?rodowisko przetwarzania generuje wyjątek IllegalThreadStateException, gdy próbujemy
wywołać metodę wątku a wątek znajduje się w takim stanie, który nie pozwala na wywołanie
tej metody. Przykładowo w stanie "nie wykonywany" wyjątek ten występuje, gdy próbujemy
wywołać metodę suspend.
Metoda isAlive
Interfejs programistyczny dla klasy Thread zawiera metodę isAlive. Wynikiem wykonania
metody isAlive jest wartość true, gdy wątek został uruchomiony a nie został jeszcze
zakończony. Gdy wynikiem wykonania metody isAlive jest wartość false oznacza to, Ŝe wątek
jest albo w stanie "nowy wątek" lub "zakończony". Gdy wynikiem jest wartość true wiemy,
Ŝe jest albo w stanie "wykonywany" lub "nie wykonywany".
Priorytet wątku
Priorytet wątku informuje program szeregujący wątki Javy (ang. Java thread scheduler), kiedy
nasz wątek powinien być wykonywany w odniesieniu do innych wątków.
Wcześniej wspomniano, Ŝe wątki wykonywane są równolegle. Jeśli konceptualnie jest to
prawda, w praktyce zazwyczaj tak nie jest. Większość komputerów posiada tylko jeden
procesor, więc w danej chwili czasu wykonywany moŜe być tylko jeden wątek i
wielowątkowość jest emulowana. Wykonanie wielu wątków na pojedynczym procesorze w
jakiejś kolejności nazywane jest szeregowaniem (ang. scheduling). ?rodowisko przetwarzania
Javy implementuje bardzo prosty, deterministyczny algorytm szeregowania znany jako
"planowanie priorytetowe" (ang. fixed priority scheduling). KaŜdemu wątkowi przypisuje się
pewien priorytet, po czym przydziela się procesor temu wątkowi, którego priorytet jest
najwyŜszy.
Gdy nowy wątek jest tworzony, dziedziczy priorytet z wątku, który go utworzył. Priorytety
wątku mogą być modyfikowane w kaŜdej chwili po utworzeniu wątku poprzez uŜycie metody
setPriority. Priorytet wątku jest liczbą typu integer o wartości większej lub równej
MIN_PRIORITY i niewiększej niŜ MAX_PRIORITY (są to stałe zdefiniowane w klasie
Thread o wartości odpowiednio 1 i 10). Im większa wartość liczby określającej priorytet, tym
priorytet wątku jest większy. W momencie, gdy wiele wątków jest gotowych do wykonania,
środowisko przetwarzania wybiera do wykonania wątek z najwyŜszym priorytetem. Tylko w
przypadku, gdy wątek zatrzymuje się, ustępuje czas procesora (metoda yield) lub przechodzi
do stanu "nie wykonywany", wątek o niŜszym priorytecie zaczyna być wykonywany. Gdy
dwa wątki o tym samym priorytecie czekają na przydzielenie im czasu procesora, program
szeregujący wybiera jeden z nich i przydziela czas według algorytmu "planowania
rotacyjnego" (ang. round-robin). W algorytmie tym ustala się małą jednostkę czasu, nazywaną
kwantem czasu lub odcinkiem czasu. Kolejka procesów gotowych do wykonania jest
traktowana jako kolejka cykliczna. Planista przydziału procesora przegląda tę kolejkę i
kaŜdemu wątkowi przydziela odcinek czasu nie dłuŜszy od jednego kwantu czasu. Gdy wątek
ma czas wykonania dłuŜszy, niŜ kwant czasu, to nastąpi przerwanie wykonywania wątku i
zostanie on odłoŜony na koniec kolejki.
Wybrany wątek będzie wykonywany do momentu, gdy jeden z poniŜszych warunków będzie
spełniony:
•
•
•
wątek o wyŜszym priorytecie znalazł się w stanie "wykonywany",
wątek ustępuje procesor lub metoda run kończy działanie,
w systemie, w którym stosuje się przydzielanie kwantów czasu, kwant czasu się
skończył.
Jeśli w jakimś momencie wątek z większym priorytetem, niŜ inne wątki w stanie
"wykonywany" znajdzie się tym w stanie, to środowisko przetwarzania Javy wybiera do
wykonania wątek z nowym najwyŜszym priorytetem.
Uwaga:
W danym momencie wątek o najwyŜszym priorytecie jest wykonywany. JednakowoŜ nie jest
to gwarantowane. Program szeregujący wątki moŜe wybrać do wykonania wątek z niŜszym
priorytetem, aby uniknąć zagłodzenia. Z tych powodów, poleganie na priorytetach nie
gwarantuje nam poprawności algorytmu.
Demony
KaŜdy wątek Javy moŜe zostać demonem (ang. daemon thread). Wątek będący demonem
zajmuje się obsługiwaniem innych wątków uruchomionych w tym samym procesie, co wątek
demona. Metoda run wątku demona jest przewaŜnie nieskończoną pętlą, w której demon
czeka na zgłoszenia zapotrzebowania na usługi dostarczane przez ten wątek.
Przykładowo, przeglądarka HotJava uŜywa do czterech demonów nazwanych "Image
Fetcher", które dostarczają obrazków z dysku lub sieci dla wątków, które tego potrzebują.
Wykonanie programu kończy się z chwilą zakończenia ostatniego wątku, który nie jest
demonem. Dzieje się tak dlatego, Ŝe gdy w programie nie istnieją juŜ inne wątki prócz
demonów, demony nie mają juŜ dla kogo dostarczać usług i ich dalsze istnienie nie ma sensu.
Aby wątek został demonem uŜywany metody setDaemon z argumentem równym true.
W celu sprawdzenia, czy wątek jest demonem uŜywana jest metoda isDaemon.
Grupowanie wątków
KaŜdy wątek Javy jest członkiem grupy wątków (ang. thread group). Grupowanie wątków w
jednym obiekcie pozwala na jednoczesne manipulowanie wszystkimi zgrupowanymi
wątkami. Przykładowo, moŜemy uruchomić (metoda start ) lub zawiesić (suspend) wszystkie
zgrupowane wątki dzięki wykonaniu jednej metody. Grupowanie wątków w Javie
zaimplementowano w klasie java.lang.ThreadGroup.
?rodowisko przetwarzania Javy umieszcza nowy wątek w grupie wątków podczas jego
tworzenia. Kiedy tworzymy nowy wątek, moŜemy środowisku przetwarzania pozwolić na
umieszczenie wątku w domyślnej grupie wątków lub moŜemy explicite zadeklarować nową
grupę wątków i dodać do niej nasz wątek. Po tym, jak wątek stał się członkiem jakiejś grupy
wątków podczas tworzenia, nie moŜna przenosić wątku do innej grupy.
Jeśli tworzymy nowy wątek bez specyfikacji jego grupy w konstruktorze, środowisko
przetwarzania automatycznie umieszcza nowy wątek w tej samej grupie co wątek, który go
utworzył. Gdy aplikacja Javy zostaje uruchomiona, środowisko przetwarzania tworzy
automatycznie obiekt ThreadGroup o nazwie 'main' (nasz wątek główny naleŜy do grupy
wątków 'main'). I gdy nie zadeklarujemy tego inaczej, wszystkie utworzone wątki staną się
członkami grupy wątków 'main'.
Uwaga:
Gdy tworzymy wątek w aplecie, nowe wątki mogą być członkami innych grup wątków niŜ
'main', zaleŜnie od przeglądarki, w której aplet jest uruchamiany. Wątki w aplecie omówione
zostaną w rozdziale: Wielowątkowość w apletach.
Jeśli chcemy utworzony wątek umieścić w grupie wątków innej niŜ domyślna, musimy tę
grupę wyspecyfikować w momencie, gdy nowy wątek jest tworzony. Klasa Thread ma trzy
konstruktory pozwalające ustawić nową grupę wątków:
public Thread(ThreadGroup grupaWatkow, Runnable target)
public Thread(ThreadGroup grupaWatkow, String name)
public Thread(ThreadGroup grupaWatkow, Runnable target, String name)
KaŜdy z tych konstruktorów tworzy nowy wątek, który jest członkiem wyspecyfikowanej
grupy wątków. Przykładowo, poniŜszy kawałek kodu tworzy nową grupę wątków
(mojaGrupaW) a następnie tworzy nowy wątek mojWatek naleŜący do tej grupy:
ThreadGroup mojaGrupaW = new ThreadGroup("Moja grupa watkow");
Thread mojWatek = new Thread(mojaGrupaW, "watek w mojej grupie");
Grupa wątków, do której dołączamy nasz wątek nie musi być grupą zadeklarowaną przez nas,
moŜe to być grupa stworzona przez środowisko wykonawcze Javy lub grupa wątków
stworzona przez program (np. przeglądarkę), w którym nasz aplet uruchomiono.
Jeśli chcemy sprawdzić, do jakiej grupy naleŜy nasz wątek, moŜemy w tym celu uŜyć metody
getThreadGroup():
grupa = mojWatek.getThreadGroup();
Wynikiem wykonania tej metody jest referencja do obiektu reprezentującego grupę wątków.
Gdy mamy dostęp do obiektu reprezentującego grupę wątków, moŜemy uzyskać róŜnego
rodzaju informacje np. jakie jeszcze inne wątki naleŜą do tej grupy, moŜemy takŜe
modyfikować wątki naleŜące do tej grupy np. moŜemy je uśpić, zatrzymać lub zakończyć w
pojedynczym wywołaniu metody np. uŜycie metody:
grupa.suspend();
w tym przypadku powoduje, Ŝe wszystkie wątki naleŜące do grupy wątków grupa zostają
uśpione.
Grupa wątków moŜe zawierać dowolną liczbę wątków. Wątki naleŜące do jednej grupy
przewaŜnie są jakoś powiązane ze sobą np. wspólnym wątkiem, który je utworzył, zadaniami
jakie wykonują w programie, lub momentem, w którym powinny zostać uruchomione i
zatrzymane.
Obiekt klasy ThreadGroup moŜe zawierać nie tylko wątki ale takŜe inne obiekty klasy
ThreadGroups. NajwyŜej w hierarchii wątków aplikacji Javy znajduje się grupa wątków o
nazwie 'main'. W grupie wątków 'main' moŜemy tworzyć nowe wątki lub grupy wątków. W
grupach wątków moŜna z kolei tworzyć następne wątki lub grupy wątków. W rezultacie
hierarchia wątków moŜe przybrać wygląd jak na poniŜszym rysunku:
Rysunek 2-6 Przykładowa hierarchia grup wątków i wątków w aplikacji Javy.
Klasa ThreadGroup zawiera metody, które moŜemy następująco posegregować:
•
•
•
metody zarządzające kolekcjami - są to metody zarządzające grupą wątków i
podgrupami zawartymi w grupie wątków,
metody operujące na grupie - metody te ustawiają i pobierają atrybuty obiektu
ThreadGroup,
metody operujące na wszystkich wątkach w grupie - ten zbiór metod wykonuje
niektóre operacje, jak uruchamianie lub zatrzymywanie, wszystkich wątków w grupie
wątków ThreadGroup.
Synchronizacja wątków
RozwaŜmy teraz przypadek, gdy wykonywane są dwa niezaleŜne wątki, które współdzielą
dane i stan kaŜdego z nich zaleŜy od stanu drugiego wątku. Jedną z takich sytuacji jest
problem typu Producent/Konsument (ang. producer/consumer), gdzie producent generuje
strumień danych, które są wykorzystywane (konsumowane) przez konsumenta. Strumień ten
stanowi wspólny zasób, wątki muszą być zatem synchronizowane.
ZałóŜmy, Ŝe producent generuje liczby od 0 do 9, które są następnie składowane w obiekcie
typu Pudelko. Producent, po włoŜeniu do pudełka liczby i wydrukowaniu jej na ekranie,
zostaje uśpiony na losowo wybrany czas między 0 a 100 milisekund, zanim przejdzie do
następnego cyklu produkcji liczby.
Przykład 2.27 Aplikacja Producent/Konsument
class Producent extends Thread
{
private Pudelko pudelko;
private int m_nLiczba;
public Producent(Pudelko c, int liczba)
{
pudelko = c;
this.m_nLiczba = liczba;
}
public void run()
{
for (int i = 0; i < 10; i++)
{
pudelko.wloz(i);
System.out.println("Producent #" + this.m_nLiczba
+ " wlozyl: " + i);
try
{
sleep((int)(Math.random() * 100));
}
catch (InterruptedException e) { }
}
}
}
Konsument podczas swego działania konsumuje wszystkie liczby złoŜone w pudełku,
wyprodukowane przez Producenta, tak szybko, jak staną się one dostępne.
class Konsument extends Thread
{
private Pudelko pudelko;
private int m_nLiczba;
public Konsument(Pudelko c, int Liczba)
{
pudelko = c;
this.m_nLiczba = Liczba;
}
public void run()
{
int wartosc = 0;
for (int i = 0; i < 10; i++)
{
wartosc = pudelko.wez();
System.out.println("Konsument #" + this.m_nLiczba
+ " wyjal: " + wartosc);
}
}
}
Producent i konsument w tym przykładzie współdzielą dane przez wspólny obiekt typu
Pudelko. Konsument ma prawo pobrać kaŜdą wyprodukowaną liczbę tylko raz.
Synchronizacja między tymi dwoma wątkami występuje w metodach wez() i wloz() obiektu
Pudelko.
Klasa Pudelko wygląda następująco:
class Pudelko
{
private int m_nZawartosc;
// to jest znienna warunkowa
// do której dostęp synchronizujemy, (omówione póŹniej)
private boolean m_bDostepne = false;
public synchronized int wez()
{
while (m_bDostepne == false)
{
try
{ wait(); }
catch (InterruptedException e) { }
}
m_bDostepne = false;
notifyAll();
return m_nZawartosc;
}
public synchronized void wloz(int wartosc)
{
while (m_bDostepne == true)
{
try
{ wait(); }
catch (InterruptedException e) { }
}
m_nZawartosc = wartosc;
m_bDostepne = true;
notifyAll();
}
}
Klasa Pudelko zostanie dokładnie omówiona póŹniej w tym rozdziale.
Po wykonaniu aplikacji ProdKonsTest, która tworzy obiekty typu Pudelko oraz Producent i
Konsument, a następnie uruchamia wątki Producenta i Konsumenta (współdzielące obiekt
typu Pudelko) :
class ProdKonsTest
{
public static void main(String[] args) throws Exception
{
Pudelko c = new Pudelko();
Producent p1 = new Producent(c, 1);
Konsument c1 = new Konsument(c, 1);
p1.start();
c1.start();
pauza();
}
static void pauza() throws java.io.IOException
{
System.out.println("Nacisnij Enter...");
System.in.read();
}
}
Na ekranie otrzymamy:
Ilustracja 2-10 Wynik działania aplikacji KonsProdTest.
W naszym programie uŜyto dwóch mechanizmów synchronizacji wątków Producenta i
Konsumenta: monitora i dwóch metod: wait oraz notifyAll.
Monitory
Obiekty takie, jak Pudelko, które są współdzielone pomiędzy dwa wątki, i do których dostęp
musi być synchronizowany nazywane są zmiennymi warunkowymi (ang. condition
variable). Java pozwala synchronizować wątki pracujące ze zmiennymi warunkowymi dzięki
uŜyciu monitorów. Monitory związane są ze specyficznymi danymi (nazywanymi zmiennymi
warunkowymi) i działają jako blokada zakładana na tych danych. Gdy wątek zajmie monitor
dla jakiejś danej, inne wątki do czasu zwolnienia monitora zostają zablokowane i nie mogą
odczytywać lub modyfikować danych.
Segment kodu w programie, w którym następuje dostęp do tej samej danej z róŜnych wątków
nazywany jest sekcją krytyczną (ang. critical section). W Javie sekcję krytyczną oznaczmy
przy uŜyciu słowa kluczowego synchronized.
Generalnie, w programach Javy, sekcją krytyczną są metody. MoŜna oznaczyć mniejszy
kawałek kodu jako sekcję krytyczną. Jednak taki sposób programowania nie jest zgodny z
paradygmatem programowania obiektowego i prowadzi do zaciemnienia kodu, który staje się
przez to trudny do zrozumienia i sprawdzenia poprawności (ang. debug). Najlepszym
rozwiązaniem jest uŜywanie synchronizacji tylko na poziomie metod.
W Javie kaŜdy obiekt, który ma metody synchroniczne posiada swój monitor. Przedstawiona
wcześniej klasa Pudelko ma dwie metody synchroniczne: metodę wloz(), uŜywaną do
zmiany wartości w obiekcie typu Pudelko i metodę wez(), która jest uŜywana do pobrania
liczby przechowywanej w obiekcie Pudelko. Oznacza to, Ŝe system skojarzy z kaŜdym
obiektem typu Pudelko unikalny monitor. W przedstawionym poniŜej kodzie klasy Pudelko
elementy wyróŜnione pogrubioną czcionką słuŜą do synchronizacji wątków:
class Pudelko
{
private int m_nZawartosc;
private boolean m_bDostepne = false;
public synchronized int wez()
{
// monitor zostaje zajęty przez Konsumenta
while (m_bDostepne == false)
{
try
{ wait(); } // metoda wait() tymczasowo zwalnia monitor,
// (dokładny opis w rozdziale poświęconym metodzie wait )
catch (InterruptedException e) { }
}
m_bDostepne = false;
notifyAll();
return m_nZawartosc;
// monitor zostaje zwolniony przez Konsumenta
}
public synchronized void wloz(int wartosc)
{
// monitor zostaje zajęty przez Producenta
while (m_bDostepne == true)
{
try
{ wait(); } // metoda wait() tymczasowo zwalnia monitor
catch (InterruptedException e) { }
}
m_nZawartosc = wartosc;
m_bDostepne = true;
notifyAll();
// monitor zostaje zwolniony przez Producenta
}
}
Klasa Pudelko posiada dwa pola danych: m_nZawartosc, które stanowi bieŜącą zawartość
pudełka oraz pole danych m_bDostepne typu boolean, które określa, czy zawartość pudełka
moŜe być pobrana. Gdy zmienna m_bDostepne jest równa true oznacza to, Ŝe Producent
właśnie umieścił nową wartość w Pudełku, a Konsument jeszcze jej nie pobrał. Konsument
moŜe pobrać daną z Pudełka tylko wtedy, gdy zmienna m_bDostępne jest równa true.
Jeśli usuniemy z metod wez() i wloz() klasy Pudelko elementy kodu odpowiedzialne za
synchronizacje (oznaczone pogrubioną czcionką). Wynik działania aplikacji ProdKonsTest
będzie następujący (lub podobny):
Ilustracja 2-11 Wynik działania aplikacji ProdKonsTest bez mechanizmów synchronizacji.
Jak widać, Konsument pobiera liczby z Pudełka nie czekając na wyprodukowanie przez
Producenta kolejnej liczby.
Wróćmy jednak do naszego przykładu, gdzie synchronizacja jest zapewniona. PoniewaŜ klasa
Pudelko ma dwie metody synchroniczne, dla kaŜdego obiektu klasy Pudelko tworzony jest
oddzielny monitor. W momencie gdy sterowanie znajdzie się w metodzie synchronicznej,
wątek, który wywołał tę metodę zajmuje monitor obiektu, którego metodę wywołano. Inne
wątki nie mogą wołać metod synchronicznych tego obiektu do czasu zwolnienia monitora.
Monitory w Javie są wielodostępne (ang. reentrant). Oznacza to, Ŝe wątek, który zajął monitor
jakiegoś obiektu moŜe wołać inne metody synchroniczne obiektu.
Zawsze wtedy, gdy Producent woła metodę wloz, Producent zajmuje monitor obiektu
Pudelko, co powoduje, Ŝe Konsument nie moŜe wywołać metody wez do czasu zwolnienia
monitora. Gdy metoda wloz kończy działanie, Producent zwalnia monitor i odblokowuje
obiekt typu Pudelko.
Podobnie gdy Konsument woła metodę wez klasy Pudelko, Konsument zajmuje monitor
obiektu typu Pudelko, co powoduje, Ŝe Producent nie moŜe wywołać metody wloz do czasu
zwolnienia monitora.
Operacje zajmowania i zwalniania monitora są wykonywane automatycznie przez środowisko
wykonawcze Javy, co zapewnia integralność danych i chroni przed wystąpieniem sytuacji
wyjątkowych spowodowanych operacjami na monitorach.
Metody notifyAll i wait
Metody wait i notifyAll naleŜą do klasy java.lang.Object i mogą być wywoływane tylko przez
wątki, które załoŜyły blokadę.
Metoda notifyAll informuje wszystkie wątki oczekujące na monitor zajęty przez bieŜący
wątek o zwolnieniu tego monitora i budzi te wątki. PrzewaŜnie jeden z oczekujących wątków
zajmuje monitor i wykonuje swoje zadanie.
W naszym przykładzie, metody wait i notifyAll słuŜą do koordynacji wkładania i wyjmowania
liczb z Pudełka. Wątek Konsumenta woła metodę wez i zajmuje monitor obiektu Pudelko na
czas wykonania metody wez. Na końcu metody wez, wywołanie metody notifyAll budzi wątek
Producenta oczekujący na monitor obiektu Pudelko. W tym momencie wątek Producenta
zajmuje monitor i wykonuje swoje zadanie.
public synchronized int wez()
{
while (m_bDostepne == false)
{
try
{ wait(); }
catch (InterruptedException e) { }
}
m_bDostepne = false;
notifyAll(); // zawiadomienie Producenta
return m_nZawartosc;
}
Gdy wiele wątków oczekuje na monitor, system wykonawczy Javy wybiera jeden z
oczekujących wątków do wykonania, nie jest jednak określone, który z oczekujących wątków
zostanie wybrany.
W metodzie wloz, notifyAll działa podobnie jak w metodzie wez, budzi wątek Konsumenta,
oczekujący na zwolnienie monitora przez Producenta.
W klasie Object jest zadeklarowana takŜe metoda notify, która budzi jeden wybrany wątek
oczekujący na monitor. W tej sytuacji, kaŜdy z pozostałych wątków oczekuje dalej do czasu
odstąpienia monitora i wybrania go przez system wykonawczy. UŜycie notify moŜe być Źle
uwarunkowane, to jest, moŜe zawieść, gdy warunki zmienią się trochę. W programowaniu
współbieŜnym ustalenie wszystkich zdarzeń, jakie mogą zajść podczas wykonania programu
jest bardzo trudne. Rozwiązanie wykorzystujące metodę notifyAll jest stabilniejsze niŜ
wykorzystujące metodę notify. W przypadku, gdy nie zamierzamy dokładnie analizować
programu wielowątkowego lepiej będzie, gdy zastosujemy metodę notifyAll.
Wątek wołający metodę wait musi posiadać monitor. Metoda wait powoduje zwolnienie
posiadanego monitora i przejście w stan oczekiwania do czasu, aŜ inny wątek powiadomi
(ang. notify) go o zwolnieniu monitora obiektu. Wtedy wątek ten przejmuje monitor i
kontynuuje swoje działanie. Metody wait uŜywamy wraz z metodami notify lub notifyAll do
koordynacji działania wielu wątków uŜywających tych samych zasobów.
W metodzie wez znajduje się pętla while. Gdy zmienna m_bDostepne jest równa false,
Producent nie wyprodukował jeszcze nowej liczby i Konsument musi czekać, więc
wywoływana jest metoda wait. Powoduje to zwolnienie monitora obiektu Pudelko i
Producent zajmuje ten monitor i moŜe wyprodukować liczbę. Gdy Producent w metodzie
wloz woła metodę notifyAll, Konsument budzi się i kontynuuje pętlę while. Jeśli liczba jest
juŜ wyprodukowana (m_bDostępne == true), pętla while zostaje opuszczona i
kontynuowane jest wykonanie metody wez.
public synchronized int wez()
{
while (m_bDostepne == false)
{
try
{
// czeka na wywołanie przez Producenta notifyAll()
wait();
}
catch (InterruptedException e) { }
}
m_bDostepne = false;
notifyAll();
return m_nZawartosc;
}
Zasada działania metody wloz jest podobna, implementuje oczekiwanie na wątek
Konsumenta aŜ skonsumuje on wyprodukowaną liczbę, gdy to nastąpi oczekujący wątek
Producenta moŜe włoŜyć następną liczbę do Pudełka.
Poza uŜytą przez nas wersją metody wait w klasie Object mamy zadeklarowane jeszcze dwie
inne wersje metody wait:
- gdzie parametr timeout oznacza maksymalny czas oczekiwania na
zwolnienie monitora (metody notify lub notifyAll) w milisekundach;
wait(long timeout)
wait(long timeout, int nanos) - gdzie parametr timeout podobnie, jak wyŜej oznacza
maksymalny czas oczekiwania na zwolnienie monitora w milisekundach a nanos dodatkowy
czas w nanosekundach o zakresie od 0 do 999999.
Metody wait i notify powinny być wołane przez wątki, które zajmują monitor danego obiektu.
Wątek moŜe stać się właścicielem monitora obiektu na trzy sposoby:
•
•
poprzez wykonanie synchronicznej metody tego obiektu,
poprzez wykonanie ciała synchronizowanego wyraŜenia, które synchronizuje obiekt,
•
dla obiektów typu Class, dzięki wykonaniu synchronizowanej metody statycznej tej
klasy.
Gdy piszemy program, w którym kilka wątków pracuje współbieŜnie¸ musimy pamiętać, Ŝe
mogą wystąpić sytuacje, w których nastąpi zagłodzenie (ang. starvation) lub zakleszczenie
wątków (ang. deadlock). Zagłodzenie występuje wtedy, gdy jeden lub więcej wątków zostaje
zablokowanych wtedy, gdy próbują uzyskać dostęp do zasobu. Zakleszczenie jest krańcową
formą zagłodzenia, występuje, gdy wątki oczekują na spełnienie warunku, który nigdy nie
moŜe być spełniony. Zakleszczenie najczęściej występuje wtedy, gdy dwa lub więcej wątków
oczekuje, aŜ inny (inne) wątek coś zrobi.
Aby uniknąć tych sytuacji naleŜy prawidłowo zaprojektować synchronizację.
Źródło:
Tutorial "Java - nowy standard programowania w Internecie" Artur Tyloch