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:
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.
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: WatekPierwszyWielowatkowy. Klasa WatekPierwszyWielowatkowy,Threadjava.lang.
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 WatekString 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
Następną metodą klasy Watekrunrunrun() klasy Watekfor 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 numersleeprandomMath. Po wykonaniu wszystkich iteracji wyświetlana jest na ekranie nazwa wątku z napisem "koniec".
Klasa PierwszyWielowatkowymain() 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
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.
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 pauzamain() metoda pauza
Wszystkie zadania, jakie ma wykonywać wątek umieszczone są w metodzie runrun.
W ciele metody runrunrun
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 ThreadRunnable. Interfejs Runnable ma tylko jedną metodę: run. Kiedy definiujemy klasę jako implementującą ten interfejs, musimy zadeklarować metodę run. W metodzie run
Przykład klasy implementującej interfejs Runnable:
class 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.
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 DrugiWielowatkowyPierwszyWielowatkowyWatekPodstawowywatkiWatekPodstawowy
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 */ }
}
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źć.
Omówmy teraz poszczególne stany:
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.
Przeanalizujmy poniższe dwie linie kodu:
Thread mojWatek = new MojaKlasaWatku();
mojWatek.start();
Metoda startrun. 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.
Wątek przechodzi do stanu "nie wykonywany" gdy zachodzi jedno z poniższych zdarzeń:
Przykładowo, wykonanie metody sleep(10000)
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
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 runwhilerun
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 mojWatekmojWatek
Thread mojWatek = new MojaKlasaWatku();
mojWatek.start();
try
{
Thread.sleep(10000);
} catch (InterruptedException e){}
mojWatek.stop();
Metoda stopThreadDeath, 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 stoprunrun wykonuje jakieś ważne obliczenia, metoda stopstop 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,isAlivefalse oznacza to, że wątek jest albo w stanie "nowy wątek". Gdy wynikiem jest wartość truewykonywany".
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 integerThreadyield)
lub przechodzi do stanu "", 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:
Jeśli w jakimś momencie wątek z większym priorytetem, niż inne wątki w stanie "wykonywany
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.
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
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.
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
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 (mojaGrupaWmojWatek 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 ThreadGroupThreadGroups. 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:
Klasa ThreadGroup
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
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 Pudelkowez()wloz()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 PudelkoProducentKonsument,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:
W naszym programie użyto dwóch mechanizmów synchronizacji wątków Producenta i Konsumenta: monitora i dwóch metod: waitnotifyAll.
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 Pudelkowloz()Pudelkowez()PudelkoPudelkoPudelko
class Pudelko
{
private int m_nZawartosc;
private boolean m_bDostepne = false;
public synchronizedwhile (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 synchronizedwhile (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_bDostepneboolean, 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ępnetrue.
Jeśli usuniemy z metod wez()wloz()PudelkoProdKonsTest
Jak widać, Konsument pobiera liczby z Pudełka
Wróćmy jednak do naszego przykładu, gdzie synchronizacja jest zapewniona. Ponieważ klasa PudelkoPudelko
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 wlozPudelko
Podobnie gdy Konsument woła metodę wez klasy PudelkoPudelkowloz 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 waitnotifyAlljava.lang.Object
Metoda notifyAll
W naszym przykładzie, metody waitnotifyAll służą do koordynacji wkładania i wyjmowania liczb z Pudełka. Wątek Konsumenta woła metodę wezPudelkowezwez, wywołanie metody notifyAllPudelko. 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 Objectnotify, 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 notifynotifyAll 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 waitwaitnotify lub notifyAll
W metodzie wezwhile. 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 PudelkowloznotifyAll, Konsument budzi się i kontynuuje pętlę while. Jeśli liczba jest już wyprodukowana (m_bDostępne == truewhilewez
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
Poza użytą przez nas wersją metody wait w klasie Objectwait:
wait(long timeout)timeoutnotifynotifyAll) w milisekundach;
wait(long timeout, int nanos) - gdzie parametr timeout podobnie, jak wyżej oznacza maksymalny czas oczekiwania na zwolnienie monitora w milisekundach a nanos
Metody waitnotify
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
Aby uniknąć tych sytuacji należy prawidłowo zaprojektować synchronizację.