Zajmiemy się teraz nowymi - dostarczanymi przez pakiet
java.util.concurrent - machanizmami synchronizacji i koordynacji
watków. Znacznie poszerzają one możliwości
współbieżnego programowania. Wprowadzają też kilka istotnych
ułatwień.
1. Przypomnienie: synchronizacja i koordynacja działania wątków
Synchronizacja
jest
mechanizmem, który zapewnia, że kilka wykonujących się
wątków nie będzie równocześnie wykonywać tego
samego
kodu, w szczególności - działać na tym samym obiekcie.
Synchronizacje jest potrzebna po to, by współdzielenie
zasobu
przez kilka wątków nie prowadziło do niespójnych
stanów zasobu.
Przykład.
Oto prosta klasa Balance, z jednym polem - liczbą całkowitą i metodą
balance(),
która najpierw zwiększa wartość tej liczby, a następnie ją
zmniejsza, po
czym zwraca wynik - wartość tej liczby.
class Balance {
private int number = 0;
public int balance() {
number++;
number--;
return number;
}
}
Wydaje się nie podlegać żadnej wątpliwości, że jakiekolwiek wielokrotne
wywoływanie
metody balance() na rzecz dowolnego obiektu klasy Balance zawsze
zwróci wartość
0.
Otóż, w świecie programowania współbieżnego nie
jest to wcale takie oczywiste!
Więcej: wynik różny od 0 może pojawiać się nader często!
Przekonajmy się o tym poprzez wielokrotne wywoływanie metody balance()
na rzecz tego samego obiektu w kilku różnych wątkach.
Każdy z wątków będziemy tworzyć i uruchamiać poprzez
stworzenie obiektu poniższej
klasy BalanceThread, dziedziczącej Thread, i wywołanie na jego rzecz
metody
start(). Przy tworzeniu nazwiemy każdy z wątków (parametr
name konstruktora).
Wielokrotne wywołania metody balance() zapiszemy w pętli w metodzie
run().
Obiekt na rzecz którego jest wywoływana metoda oraz liczbę
powtórzeń pętli
przekażemy jako dwa pozostałe argumenty konstruktora.
Tuż przed zakończeniem metody run() pokażemy jaki był wynik ostatniego
odwołania do metody balance().
class BalanceThread extends Thread {
private Balance b; // referencja do obiektu klasy Balance
private int count; // liczba powtórzeń pętli w metodzie run
public BalanceThread(String name, Balance b, int count) {
super(name);
this.b = b;
this.count = count;
start();
}
public void run() {
int wynik = 0;
// W pętli wielokrotnie wywołujemy metodę balance()
// na rzecz obiektu b klasy Balance.
// Jeżeli wynik metody jest różny od zera - przerywamy działanie pętli
for (int i = 0; i < count; i++) {
wynik = b.balance();
if (wynik != 0) break;
}
// Pokazujemy wartość zmiennej wynik na wyjściu z metody run()
System.out.println(Thread.currentThread().getName() + " konczy z wynikiem " + wynik);
}
}
W klasie testującej stworzymy obiekt klasy Balance, po czym stworzymy i
uruchomimy
podaną przez użytkownika liczbę wątków, które za
pomocą metody run() z klasy
BalanceThread będą równolegle operować na tym obiekcie
wielokrotnie wywołując
na jego rzecz metodę balance() z klasy Balance.
class BalanceTest {
public static void main(String[] args) {
int tnum = Integer.parseInt(args[0]); // liczba wątków
int count = Integer.parseInt(args[1]); // liczba powtórzeń pętli w run()
// Tworzymy obiekt klasy balance
Balance b = new Balance();
// Tworzymy i uruchamiamy wątki
Thread[] thread = new Thread[tnum]; // tablica wątków
for (int i = 0; i < tnum; i++)
thread[i] = new BalanceThread("W"+(i+1), b, count);
// czekaj na zakończenie wszystkich wątków
try {
for (int i = 0; i < tnum; i++) thread[i].join();
} catch (InterruptedException exc) {
System.exit(1);
}
System.out.println("Koniec programu");
}
}
Uwaga: metoda join z klasy Thread powoduje
oczekiwanie na zakończenie
wątku, na rzecz któego została wywołana. Oczekiwanie może
być przerwane,
gdy wątek został przerwany przez inny wątek - wtedy wystąpi wyjątek
InterruptedException.
Uruchamiając aplikację z podanymi jako argumenty liczbą wątkow = 2 oraz
liczbą
powtorzeń pętli w metodzie run() = 100000, nader często zyskamy
intuicyjnie
oczekiwany wynik (W1 konczy z wynikiem 0, W2 konczy z wynikem 0). Może
się
jednak zdarzyć wynik inny! Zwiększenie liczby wątków i
liczby powtórzeń pętli
prawie na pewno szybko pokaże nam, że niektóre wątki
zakończą działanie z wynikem
różnym od 0.
Na przyklad, przy liczbie wątkow = 5 i liczbie powtórzeń
pętli = 1000000, możemy raz uzyskac następujący wynik:
W2 konczy z wynikiem 0
W3 konczy z wynikiem 0
W4 konczy z wynikiem 0
W1 konczy z wynikiem 0
W5 konczy z wynikiem 0
a za chwilę, przy ponowym uruchomieniu z tymi samymi argumentami:
W1 konczy z wynikiem 1
W3 konczy z wynikiem 1
W2 konczy z wynikiem 1
W5 konczy z wynikiem 0
W4 konczy z wynikiem 0
Powstaje oczywiste pytanie: jak to się dzieje, że w powyższym
przykładowym
programie uzyskujemy wyniki, których - wydaje się na
podstawie analizy kodu
metody balance() - nie sposób uzyskać?
Otóż, wszystkie wykonujące (tę samą) metodę run() wątki
odwołują się do tego
samego obiektu klasy Balance (w programie oznaczanego przez b).
Mówimy: współdzielą
obiekt.
Obiekt ten ma jeden element - odpowiadający zmiennej number
zdefiniowanej jako pole klasy Balance.
Wywolywana przez wątki na rzecz tego obiektu metoda balance()
zwiększa a następnie zmniejsza wartość tej zmiennej.
Wyobraźmy sobie, że działają dwa wątki. Jeden z nich uzyskuje czas
procesora
i rozpoczyna wykonanie metody balance(). Po zwiększeniu o 1 zmiennej
number
zostaje wywlaszczony (zmienna number ma teraz wartość 1). Czas
procesora
przydzielany jest drugiemu wątkowi. Drugi wątek rozpoczyna wykonanie
metody
balance() i zwiększa o 1 wartość zmiennej number (zmienna number ma
teraz
wartość 2), po czym zostaje wywlaszczony, a czas procesora przydzielony
zostaje
wątkowi pierwszemu, który kontynuuje wykonanie metody run()
od miejsca, w
którym odebrano mu czas procesora. Zmniejsza on teraz
zmienną number i zwraca
wynik, który równy jest 1. Po zakończeniu wątku
pierwszego, pracę kontynuuje
wątek drugi. Po zmniejszeniu zmiennej number zwraca wynik 0.
Obrazuje to poniższa animacja.
Komentarze:
Punkt a - wątek W1, po zwiększeniu zmiennej number zostaje wywłaszczony. Zaczyna działanie wątek W2
Punkt b - wątek W2 po zwiększeniu zmiennej number zostaje wywłaszony.
Pracę kontunuuje wątek W1 (mając za wartość zmiennej number 2)
Punkt c - po zakończeniu wątku W1 (który zmniejszył number i zwrócił
1), wątek W2 kontunnuje pracę: zmniejsza number i zwraca wynik 0.
Jest to sytuacja, która może się zdarzyć, ale nie musi. Wszystko zależy od
tego czy i kiedy (w jakim momecie wykonania metody run()) systemowy zarządca
wątków wywlaszczy wykonujący się aktualnie wątek. A z tym jest bardzo różnie
w zależności od platformy systemowej czy aktualnego obciążenia procesora.
Aby uniknąć równoczesnego działania wątków na tym samym obiekcie (co w sposób
nieprzewidywalny ukształtować może jego stany) stosuje się synchronizację.
Synchronizację wątków uzyskuje się za pomocą
obiektów, które wykluczają
równoczesny
dostęp wątków do zasobów i/lub
równoczesne wykonanie przez wątki tego samego
kodu. Takie obiekty nazywają się ogólnie synchronizatorami
lub muteksami (od ang. mutual-exclusion semaphore).
W Javie rolę synchronizatorów pełnią rygle (ang
lock).
Do wersji 1.5 synchronizację wątków można było uzyskać
wyłącznie za pomocą słowa kluczowego synchronized.
Sposoby użycia.
Synchronizowane mogą być metody i bloki.
Metoda synchronizowana oznaczana jest w deklaracji słowem synchronized:
synchronized void metoda() {
...
}
Bloki synchronizowane wprowadzane są instrukcją synchronized z podaną w nawiasie referencją do obiektu, który ma być zaryglowany.
synchronized (lock) {
// ... kod
}
gdzie: lock - referencja do ryglowanego obiektu
kod - kod bloku synchronizowanego
Kiedy dany wątek wywołuje na rzecz jakiegoś obiektu metodę
synchronizowaną, automatycznie zamykany jest rygiel. Mówimy też:
obiekt jest zajmowany przez wątek.
Inne wątki usiłujące wywołać na rzecz tego obiektu metodę synchronizowaną
(niekoniecznie tę samą, ale koniecznie synchronizowaną) lub też wykonać instrukcję synchronized z podanym odniesieniem do zaryglowanego obiektu (o tej instrukcji za chwilę) są blokowane i czekają
na zakończenie wykonania metody przez wątek, który zajął obiekt (zamknął rygiel).
Dowolne zakończenie metody synchronizowanej (również na skutek powstania wyjątku) zwalnia rygiel, dając czekającym
wątkom możność dostępu do obiektu. Mogą tez być inne przyczyny zwolnienia
rygla, o których będzie mowa w podrozdziale o stanach wątków).
Z kolei wykonanie instrukcjisynchronized przez wątek rygluje obiekt, do którego referencja podana jest w nawiasach tej instrukcji.
Inne wątki, które usiłują operowac na tym obiekcie za pomocą metod synchronizowanych
lub wykonać instrukcję synchronized z referencją do tego obiektu są blokowane
do chwili gdy wykonanie kodu bloku synchronizowanego nie zostanie zakończone
przez wątek zajmujący obiekt (lub wątek ten nie zwolni rygla na skutek innych
przyczyn).
O ryglowaniu (wprowadzanym za pomocą słowa kluczowego
synchronized) możemy
myśleć jako o zapewnieniu wyłącznego dostępu do pól obiektu
lub (statycznych)
pól klasy, ale równie dobrze "zaryglowany" obiekt
może spelniać rolę muteksu,
zabezpieczającego fragment kodu przed równoczesnym
wykonaniem przez dwa wątki.
Kod, który może być wykonywany w danym momencie tylko przez
jeden wątek nazywa się sekcją krytyczną.
W Javie sekcje krytyczne wprowadza się jako bloki lub metody
synchronizowane.
Użycie sekcji krytycznych pozwala na prawidłowe
współdzielenie zasobów przez wątki.
W polskiej literaturze przedmiotu używa się także terminów:
zajmowanie zasobu (obiektu) przez wątek,
wzajemne wykluczanie wątków w dostępie do zasobu
(obiektu).
Pojęcia te można traktowac jako szczególne przypadki
synchronizacji, a ponieważ
prawidłowe współdzielenie zasobów jest w
programowaniu współbieżnym kluczowe,
to często utożsamiamy je z synchronizacją. Przez
synchronizację wątków
Dlatego mowa o synchronizacji. w Javie rozumiemy więc
najczęściej
wzajemne
wykluczanie wątkow w dostępie do obiektów. W przeciwieństwie
do
asynchronicznego,
dowolnego w czasie, równoległego dostępu, synchronizacja
oznacza
sekwencyjny,
kolejny w czasie dostęp wątków do zasobów. Slowo
to jest
również wygodne ze względu na łatwe kojarzenie ze słowem
kluczowym synchronized.
Nie należy jednak sądzić, że synchronizacja wątków oznacza
zagwarantowanie
określonej, konkretnej kolejności dostępu
wątków do wspóldzielonych zasobów.
Ustalanie i kontrolowanie konkretnej kolejności dostępu wątkow (często
zależnej
od wyników wytwarzanych przez wykonywane przez nie kody) do
wspóldzielonych
zasobów będziemy nazywać koordynacją
wątków.
2. Przypomnienie: koordynacja wątków
Ryglowanie (użycie synchronized) służy do zapobiegania niepożądanej interakcji wątków.
Nie jest ono jednak wystarczającym środkiem dla zapewnienia współdziałania wątków.
Przykład:
Dwa wątki Author i Writer mogą odwoływać się do tego samego obiekty typu Teksty.
Author podrzuca teksty, zapisywane w polu txt, Writer wypisuje je na konsoli.
Do ustalania tekstów służy metoda setTextToWrite (wywołuje ją Author), teksty
do zapisu odczytywane są przez Writera za pomocą metody getTextToWrite i
wypisywane na konsoli.
Ponieważ metody te mogą być wywołane równocześnie (z różnych wątków) i operują
na polu tego samego obiektu, winny być synchronizowane.
Ale tu ważna jest również kolejność i koordynacja działań obu wątków.
Chodzi o to, by Writer zapisywał tylko raz to co poda Autor, a Autor nie
podawał nic nowego, dopóki Writer nie zapisze poprzednio podanego tekstu.
Skoordynowanie interakcji pomiędzy wątkami w Javie do wersji 1.5 uzyskiwało się za pomocą metod klasy Object:
wait
notify
notifyAll
W tej konwencji koordynacja działań wątków sprowadza się do następujących kroków:
Wątek wywołuje metodę wait na rzecz danego obiektu, gdy oczekuje,
że ma się coś (zwykle w kontekście tego obiektu) zdarzyć (zwykle jest to
pewna oczekiwana zmiana stanu obiektu, której ma dokonać inny wątek i która
jest realizowana np. przez zmianę wartości jakiejś zmiennej - pola obiektu).
Wywołanie wait blokuje wątek (jest on odsuwany od procesora), a jednocześnie powoduje otwarcie rygla
zajętego przez niego obiektu, umożliwiające dostęp do obiektu z innych wątków
(wait może być wywołane tylko z sekcji krytycznej, bowiem chodzi tu
o współdziałanie wątków na tym samym ryglowanym obiekcie, a zatem konieczna
jest synchronizacja). Inny wątek może teraz zmienić stan obiektu i powiadomić
o tym wątek czekający (za pomocą metody notify lub notifyAll).
Odblokowanie (przywrócenie gotowości działania i ew. wznowienie działania wątku) następuje, gdy inny wątek wywoła metodę notify lub notifyAll na rzecz tego samego obiektu, "na którym" dany wątek czeka (na rzecz którego wywołał metodę wait).
Wywołanie notify() odblokowuje jeden z czekających wątków, przy czym może to być dowolny z nich,
Metoda notifyAll odblokowuje wszystkie czekające na danym obiekcie wątki,
Wywołanie notify lub notifyAll musi być także zawarte w sekcji krytycznej.
Metoda wait() może mieć argument, który specyfikuje maksymalny czas
oczekiwania. Po upływie tego czasu wątek zostanie odblokowany, niezależnie
od tego czy użyto jakiegoś notify() wobec obiektu na którym było synchronizowane
wait.
Spójrzmy na przykład (schemat) prawidłowej koordynacji:
class X {
int n;
boolean ready = false;
....
synchronized int get() {
try {
while(!ready)
wait();
} catch (InterruptedException exc) { .. }
ready = false;
return n;
}
synchronized void put(int i) {
n = i;
ready = true;
notify();
}
}
Uwaga: metoda wait() może sygnalizować wyjątek InterruptedException (w
przypadku, gdy nastąpiło zewnętrzne przerwanie oczekiwania na skutek użycia
w innym wątku metody interrupt()). Wyjątek ten musimy obsługiwać.
Wyobraźmy sobie, że działają tu dwa wątki - ustalający wartość n za pomocą put i pobierający wartość n za pomocą get.
Wątek pobierający musi czekać, aż wątek ustalający ustali wartość n (wait).
Ustalenie wartości powoduje dwie zmiany: warunek "wartość gotowa" staje się true, a oczekiwanie jest przerywane przez notify.
Zwróćmy uwagę, że metody wait i notify są wywoływane na rzecz tego obiektu,
na rzecz którego wywołano metody get i put (moglibyśmy napisać - dla większej
jasności: this.wait() i this.notify()) i na tym obiekcie właśnie wątki będą synchronizowane.
Czy naprawdę oprócz sygnału notify potrzebny jest warunek "wartość gotowa"?
W prostym przypadku wystarczyłoby być może samo notify.
Schemat pokazuje jednak ogólniejszą konstrukcję, kiedy samo
notify (które może przyjść od różnych wątków) nie
wystarcza.
Oczekiwanie kończy się naprawdę dopiero wtedy, gdy spełniony jest jakiś warunek.
UWAGA: warunek zakończenia oczekiwania należy sprawdzać w pętli. Nie ma bowiem
gwarancji, że po odblokowaniu wątku czekającego warunek nadal będzie spełniony.
Zgodną z przedstawionym schematem realizację omówionego wcześniej
przykładu Author-Writer pokazano na wydruku poniżej. W programie wątek-Autor
co jakiś czas (generowany losowo) ustala tekst do napisania (są to kolejne
elementy tablicy napisów). Wątek-Writer pobiera ustalony tekst i wypisuje
na konsoli. Zakończenie pracy Autor sygnalizuje poprzez podanie tekstu =
null.
// Klasa dla ustalania i pobierania tekstów
class Teksty {
String txt = null;
boolean newTxt = false;
// Metoda ustalająca tekst - wywołuje Autor
synchronized void setTextToWrite(String s) {
while (newTxt == true) {
try {
wait();
} catch(InterruptedException exc) {}
}
txt = s;
newTxt = true;
notifyAll();
}
// Metoda pobrania tekstu - wywołuje Writer
synchronized String getTextToWrite() {
while (newTxt == false) {
try {
wait();
} catch(InterruptedException exc) {}
}
newTxt = false;
notifyAll();
return txt;
}
}
// Klasa "wypisywacza"
class Writer extends Thread {
Teksty txtArea;
Writer(Teksty t) {
txtArea=t;
}
public void run() {
String txt = txtArea.getTextToWrite();
while(txt != null) {
System.out.println("-> " + txt);
txt = txtArea.getTextToWrite();
}
}
}
// Klasa autora
class Author extends Thread {
Teksty txtArea;
Author(Teksty t) {
txtArea=t;
}
public void run() {
String[] s = { "Pies", "Kot", "Zebra", "Lew", "Owca", "Słoń", null };
for (int i=0; i<s.length; i++) {
try { // autor zastanawia się chwilę co napisać
sleep((int)(Math.random() * 1000));
} catch(InterruptedException exc) { }
txtArea.setTextToWrite(s[i]);
}
}
}
// Klasa testująca
public class Koord {
public static void main(String[] args) {
Teksty t = new Teksty();
Thread t1 = new Author(t);
Thread t2 = new Writer(t);
t1.start();
t2.start();
}
}
Warto skompilować program i prześledzić jego działanie.
Następnie, po usunięciu konstrukcji while(newTxt == ... ) oraz wait() i notifyAll()
w metodach setTxtToWrite i getTxtToWrite skompilowac oraz uruchomić program
ponownie i przekonać się, że sama synchronizacja wątków (pozostawiamy obie
metody jako synchronizowane) nie wystarcza dla zapewnienia właściwej kolejności
działań.
Pojęcie monitora
W Javie z każdym obiektem oprócz rygla związana jest "kolejkę oczekiwania" (wait set). Ogólnie
kolejka ta zawiera odniesienia do wszystkich wątków zablokowanych na obiekcie
(metodą wait) i czekających na powiadomienie o możliwości wznowienia działania.
Kolejka oczekiwania jest "prowadzona" przez JVM, a jej zmiany mogą być uzyskane
tylko metodami wait, notify, notifyAll (z klasy Object) oraz interrupt (z
klasy Thread).
Twory, które mają rygle i kolejki oczekiwania nazywane są monitorami.
Generalnie - monitor jest fragmentem kodu programu (niekoniecznie ciągłym),
do którego dostęp zabezpieczany jest przez rygiel (muteks). W odróżnieniu
od sekcji krytycznych - monitory są powiązane z obiektami, ich stanami. Dlatego
mówimy czasem krótko: "obiekt ma monitor", "monitor obiektu" lub nawet "obiekt jest monitorem".
3. Rygle jako obiekty typu Lock
W Javie 1.5 oprócz wcześniej dostępnych środków synchronizacji wątków - pojawiły się nowe.
Teraz rygiel może być obiektem klasy implementującej interfejs Lock. W pakiecie java.util.concurrent mamy dwie takie klasy:
ReentrantLock - odpowiada znanemu nam mechanizmowi synchronizacji za pomocą słowa kluczowego synchronized
ReentrantReadWriteLock -
realizuje koncepcję tzw. read/write locks, pozwalającą na
współdzielenie zasobu bez blokowania przy operacjach
czytania i jednocześnie zapewnienie, aby operacje modyfikacji były
zsynchronizowane z odczytem (natychmiastowo widoczne dla wątków
czytających).
Nowe rygle wydają się na pierwszy rzut oka bardziej przejrzyste niż użycie synchronizowanych
metod, operujemy bowiem w sposób jawny na obiektach-ryglach, a
nie na domyślnie (i niewidocznie) skojarzonych z obiektami ich
ryglach.
Mają one i inne zalety:
są bardziej efektywne od synchronized w sytuacji dużej konkurencji wątków o zasoby,
jako "zwykłe" obiekty Javy mogą być dostępne przez referencje w wielu
miejscach kodu (np. przekazywane jako argumenty konstruktorów
czy metod),
mogą być zamykane i zwalniane w różnych strukturalnie sekcjach
kodu np. w różnych metodach (ale wykonywanych przez ten sam
wątek), synchronized może być użyte tylko w ramach tego samego bloku,
możliwe jest sprawdzenie, czy rygiel jest zamknięty (inny wątek
wykonuje sekcję krytyczną) i np. wtedy zajęcie się innymi czynnościami,
a nie blokowanie bieżącego wątku na ryglu, mamy tu możliwości zastosowania metody tryLock() lub bezpośrednie odpytywanie obiektu-rygla czy jest zamknięty,
możliwe jest oczekiwanie na uzyskanie dostępu do sekcji krytycznej
(otwarcie rygla przez inny wątek) przez określony czas (nie chcemy
blokować bieżącego wątku zbyt długo), służy do tego metoda tryLock(...) z podanym czasem oczekiwania,
możliwe jest przerwanie wątku zablokowanego na jakimś ryglu, tu używamy metody lockInterruptibly() (synchronized nie daje tej możliwości).
Podstawowy schemat działania.
Lock lock = new ReentrantLock(); // utworzenie rygla
// ....
// KOD WYKONYWANY WSPÓŁBIEŻNIE
lock.lock(); // zamknięcie rygla (1)
// ... kod sekcji krytycznej
lock.unlock(); // zwolnienie rygla (2)
Niech w jakimś wątku wykonywana jest instrukcja (1) (lock.lock()).
Rygiel jest zamykany i wykonywany jest kod sekcji krytycznej. Inne
wątki, które chcą wejść w ten kod (wykonać instrukcję
lock.lock()) będą blokowane na tym wywołaniu dopóki właściciel
rygla (ten kto go zamknął) nie dobiegnie do końca sekcji krytycznej i
nie zwolni rygla (lock.unlock()).
W ten sposób sekcja krytyczna może być wykonywana od początku do końca tylko w jednym wątku.
Są tu pewne podobieństwa do działania bloków synchronizowanych, ale też i znaczące różnice.
Przede wszystkim, jeżeli w kodzie sekcji krytycznej wystąpi wyjątek, to
blokada rygla może nie zostać zwolniona i inne wątki pozostaną na tym
ryglu zablokowane na zawsze. Jest to błąd trudny do wykrycia.
Zobaczmy:
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
class StringTab {
private String[] txt;
private Lock lock = new ReentrantLock();
public StringTab(String[] txt) {
this.txt = txt;
}
public void set(int i, String s) {
lock.lock();
txt[i] = s;
lock.unlock();
}
public String get(int i) {
String t = null;
lock.lock();
t = txt[i];
lock.unlock();
return t;
}
}
public class Test1 {
public static void main(String[] args) {
final StringTab st = new StringTab(new String[] { "ala", "kot", "pies" });
new Thread( new Runnable() {
public void run() {
st.set(3, "tygrys");
}
}). start();
new Thread( new Runnable() {
public void run() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Minęły 2 sek. - Wątek 2 działa");
System.out.println(st.get(0));
System.out.println("Wątek 2 się kończy");
}
}). start();
}
}
Wynik działania programu:
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 3
at locksIntro.StringTab.set(Test1.java:18)
at locksIntro.Test1$1.run(Test1.java:39)
at java.lang.Thread.run(Unknown Source)
Minęły 2 sek. - Wątek 2 działa
W tym programie zastosowaliśmy synchronizację w dostępie do
wspólnego zasobu - tablicy Stringów. Słusznie, ponieważ
kody metod get i set z klasy StringTab mogą być wykonywane
współbieżnie przez wiele wątków.
Jednak kod wykonywany w jednym z wątków (ustalający wartość
elementu tablicy o indeksie 3) spowodował wyjątek
ArrayIndexOutOfBoundsException (nie ma indeksu 3!) i kod ten zostaje
przerwany przed zwolnieniem rygla. Drugi wątek już wystartował, ale
czeka sobie 2 sekundy (proszę zwrócić uwagę na możliwość użycia
nowej formy wywołania metody sleep()). Po tych dwóch sekundach
wyprowadza komunikat i próbuje wykonać metodę get. Metoda ta -
jak widać - próbuje zamknąć rygiel (dostęp do sekcji
krytycznej), ale ten już jest zamknięty przez pierwszy wątek. Wątek
drugi będzie wiecznie zablokowany na tym ryglu, bowiem jego właściciel
(ten kto go zamknął) czyli pierwszy wątek już dawno zginął i nie może
go otworzyć.
Błędy mogą być subtelne. Wydaje się oczywistością, że w metodach set i
get powinniśmy się bronić przed wyjątkiem
ArrayIndexOutOfBoundsException. Oczywistym sposobem jest przekazanie
obsługi do wywołującego (czyli dodanie w sygnaturach klauzuli throws
...). To oczywiście nie pomoże, bo rygiel pozostanie zamknięty. A może
obsłużyć wyjątek wewnątrz metody? Np.
public void set(int i, String s) {
lock.lock();
try {
txt[i] = s;
} catch (ArrayIndexOutOfBoundsException exc) {}
lock.unlock();
}
To rozwiąże nam problem tego konkretnego programu (będzie działał
dobrze). Ale nie jest uniwersalnym rozwiązaniem, ani nawet dobrym. Po
pierwsze nawet tu mogą się pojawiać inne wyjątki (np.
NullPointerException, gdy przekażemy w konstruktorze jako referencję do
tablicy null). Po drugie, metody mogą być bardziej skomplikowane, wołać
inne i tam może się pojawić jakiś wyjątek klasy RuntimeException. Po
trzecie wreszcie, decydowanie o tym co zrobić gdy podano niewłaściwiy
argument (tu wadliwy indeks) należy zawsze pozostawić wołającemu (czyli
jednak throws... lub sygnalizacja wyjątku typu RuntimeException ).
4. Konieczność użycia klauzuli finally. Klasa ReentrantLock.
Elegenckim rozwiązaniem opisanego wyżej problemu jest użycie bloku try i klauzuli finally.
public void set(int i, String s) {
lock.lock();
try {
txt[i] = s;
} finally {
lock.unlock();
}
}
Takiej konstrukcji powinniśmy używać zawsze, nawet jeśli nie liczymy się z wystąpieniem jakichkolwiek wyjątków.
Ma ona bowiem znaczenie nie tylko wtedy, gdy ew. wyjątki mogą wystąpić,
ale również jest jedynym sposobem poprawnego oprogramowanie
metod, które zwracają wynik.
Oto może się wydać, że konstrukcja:
Lock lock = new ReentrantLock();
public typ metoda() {
typ zmienna;
lock();
// ...
unlock();
return zmienna;
}
jest równoważna:
Object mutex = new Object();
public typ metoda() {
typ zmienna;
synchronized(mutex) {
// ...
return zmienna;
}
}
Nic błędniejszego.
Zobaczmy na znanym nam już przykładzie, w którym wielokrotne
wywołanie w pętli przez różne wątki metody balance() daje
nieoczekiwany wynik (różny od 0), jesli nie ma
synchronizacji.
class Balance {
private int number = 0;
private Lock lock = new ReentrantLock();
public int balance() {
lock.lock();
number++;
System.out.print("*");
number--;
lock.unlock();
return number;
}
}
class BalanceThread extends Thread {
private Balance b; // referencja do obiektu klasy Balance
private int count; // liczba powtórzeń pętli w metodzie run
public BalanceThread(String name, Balance b, int count) {
super(name);
this.b = b;
this.count = count;
start();
}
public void run() {
int wynik = 0;
for (int i = 0; i < count; i++) {
wynik = b.balance();
if (wynik != 0) break;
}
System.out.println("\n"+ Thread.currentThread().getName() +
" konczy z wynikiem " + wynik);
}
}
class BalanceTest {
public static void main(String[] args) {
int tnum = Integer.parseInt(args[0]); // liczba wątków
int count = Integer.parseInt(args[1]); // liczba powtórzeń pętli w run()
// Tworzymy obiekt klasy balance
Balance b = new Balance();
// Tworzymy i uruchamiamy wątki
Thread[] thread = new Thread[tnum];
long start = System.nanoTime();
for (int i = 0; i < tnum; i++)
thread[i] = new BalanceThread("W"+(i+1), b, count);
// czekaj na zako˝czenie wszystkich w?tkˇw
try {
for (int i = 0; i < tnum; i++) thread[i].join();
} catch (InterruptedException exc) {
System.exit(1);
}
System.out.println("Czas: " + (System.nanoTime() - start) );
}
}
Niby jest synchronizacja, a jednak niektóre wątki kończą z wynikiem różnym od 0, np.
W42 konczy z wynikiem 0
*
W39 konczy z wynikiem 1
Okazuje się, że return
znajdujące się poza sekcją krytyczną może zwrócić wynik,
który produkuje już inny wątek: Wygląda to tak: wątek A zamknął
rygiel wykonał obliczenia, otworzył rygiel i w tym momencie został
wywłaszczony. Wątek B rozpoczyna obliczenia, w momecie wypisywania
gwiazdki (blokowanie na we/wy) jest wywłaszczany, a wątek A wraca do
procesora i zwraca wartość zmiennej number, którą przed chwilą
ustalił wątek A.
A zatem koniecznie należy użyć konstrukcji try - finally, chociaż w tym
przykładzie nie ma żadnej możliwości pojawienia się wyjątków.
public int balance() {
try {
lock.lock();
number++;
System.out.print("*");
number--;
return number;
} finally {
lock.unlock();
}
}
W tym przypadku wykonanie return zostanie wstrzymane do chwili zakończenia bloku finally i uzyskamy właściwe wyniki.
Tak samo powinniśmy poprawić metodę get(...) z klasy StringTab.
Zamiast niebezpiecznego:
public String get(int i) {
String t = null;
lock.lock();
t = txt[i];
lock.unlock();
return t;
}
Należy podkreślić, że zastosowanie try/finally musi być w pełni świadome, bo obarczone jest dodatkowymi
niebezpieczeństwami.
Po pierwsze, jeśli pojawi
się wyjątek, który przerwie wykonanie bloku try, to po
zwolnieniu rygla w klauzuli finally stan obiektu może być
niespójny. Dlatego w finally zawsze nalezy dostarczyć
odpowiednich operacji porządkujących.
Po drugie, w przypadku,
gdy stosujemy ryglowanie przerywalne (lockInterruptibly() lub
tryLock(...) z podanym czasem), to po przerwaniu wątku (metodą
interrupt()) rygiel jest otwierany i wątek już go "nie posiada";
jednak finally jest wykonywane i próba zwolnienia rygla w
tym miejscu powoduje wyjątek IllegalMonitorStateException (próba
zwolnienia nieposiadanego rygla).
Ilustacją pierwszego przypadku może być zmodyfikowany kod przykładu z bilansowaniem liczby.
Niech np. oprócz dodawania i odejmowania wykonywane są jeszcze jakieś inne operacje:
int w;
int innaLiczba;
// ....
// Kod metody balance
lock.lock();
try {
number++;
if (print) System.out.print("*");
w = number/innaLiczba;
number--;
return number;
} finally {
lock.unlock();
}
Jesli innaLiczba = 0, to powstanie wyjątek ArithmeticException, blok
try zostanie przerwany, w finally zwolniony rygiel, ale stan obiektu
Balance będzie niespójny (założenie - po wykonaniu metody
balance ma to być 0). Inne wątki, które opierają się na tym
założeniu mogą prowadzić do dalszych złych (coraz gorszych)
wyników.
Przykładowy wynik działania kilku wątków wykonujących kod:
public void run() {
int wynik = 0;
for (int i = 0; i < count; i++) {
boolean print;
if (i%20 == 0) print = true;
else print = false;
try {
wynik = b.balance(print);
} catch(Exception exc) { }
if (wynik != 0) break;
}
System.out.println("\n"+ Thread.currentThread().getName() +
" konczy z wynikiem " + wynik);
System.out.println("Stan b: " + b);
}
}
*****
L1 konczy z wynikiem 0
Stan b: 100
*****
L2 konczy z wynikiem 0
Stan b: 200
*****
L3 konczy z wynikiem 0
Stan b: 300
*****
L4 konczy z wynikiem 0
Stan b: 400
*****
L5 konczy z wynikiem 0
Stan b: 500
*****
L6 konczy z wynikiem 0
Stan b: 600
*****
L7 konczy z wynikiem 0
Stan b: 700
*****
L8 konczy z wynikiem 0
Stan b: 800
*****
L9 konczy z wynikiem 0
Stan b: 900
*****
L10 konczy z wynikiem 0
Stan b: 1000
Warto przy okazji zwrócić uwagę, że działanie metody balance()
jest przerywane jeszcze przed zwrotem wyniku, a to co pokazuje "wynik"
wątku jest inicjalną wartością nadaną w metodzie run(). Faktyczny stan
obiektu b jest - jak widać zupełnie inny.
Może więc (nawet w tym prostym przypadku) trzeba napisać tak:
public int balance1() {
lock.lock();
boolean balanced = true;
try {
number++;
balanced = false;
w = number/innaLiczba;
number--;
balanced = true;
return number;
} finally {
if (!balanced) number--;
lock.unlock();
}
}
Drugi przykład dotyczy przerywania blokowania na ryglu.
Mamy oto jakiś wątek, który zamyka rygiel w sposób przerywalny.
Jeśli po uruchomieniu tego zadania zostanie ono zablokowane na ryglu i
przerwiemy go (posyłając do odpowiedniego wątku interrupt()
(bezpośrednio lub za pośrednictwem ExecutorService), to uzyskamy taki
oto obrazek:
Aby uniknąć takich niespodzianek zwalniając rygiel w finally winniśmy
sprawdzić, czy nadal przynależy on do bieżącego wątku, stosując metodę isHeldByCurrentThread().
Lock lock = new ReentrantLock();
Runnable task1 = new Runnable() {
public void run() {
System.out.println("Task 1 begins");
try {
lock.tryLock(10, TimeUnit.SECONDS); // albo lock.lockInterruptibly();
System.out.println("Task 1 entered");
} catch(InterruptedException exc) {
System.out.println("Task 1 interrupted");
} finally {
ReentrantLock l = (ReentrantLock) lock;
if (l.isHeldByCurrentThread()) lock.unlock();
}
System.out.println("Task 1 stopped");
}
};
Ta metoda nie jest metodą interfejsu Lock, ale klasy go implementującej - ReentrantLock.
Reentrant (wielobieżny) oznacza możliwość ponownego wykonania tej
samej operacji przez ten sam wątek w sytuacji, gdy jest ona w trakcie
wykonywania w tym wątku. W przypadku rygli chodzi o możliwość ponownego
wprowadzania tej samej sekcji krytycznych (ponownego zamykania rygla)
przez wątek, który już ten rygiel zamknął
A więc rygiel może być zamykany wielokrotnie przez ten sam wątek
(właściciel rygla nie czeka na zamkniętym przez siebie ryglu, inne
wątki czekają). Zliczana jest liczba zamknięć (możemy się dowiedzieć
jaka ona jest za pomocą metody int getHoldCount()).
Każde otwarcie rygla (unlock()) zmniejsza tę liczbę. Dopiero gdy jest
równa 0 - inne wątki mogą uzyskać dostęp do sekcji krytycznej.
5. Idiomy ryglowania
Ostateczenie można przedstawić kilka idiomatycznych, właściwych sposóbów postępowania z ryglami typu Lock.
Sekcja krytyczna z nieprzerywalną blokadą
Lock lock = new ReentrantLock();
....
lock.lock();
try {
// ...
| finally {
// ... zapewnienie spójności stanów obiektu
lock.unlock();
}
Sekcja krytyczna z przerywalną blokadą
Lock lock = new ReentrantLock();
....
try {
lock.lockInterruptibly();
// ...
| catch(InterruptedException exc) {
// ... przerwanie działania - zakończenie metody run
finally {
// ... zapewnienie spójności stanów obiektu
ReentrantLock l = (ReentrantLock) lock;
if (l.isHeldByCurrentThread()) lock.unlock();
}
6. Efektywność synchronizacji
Warto zauważyć, że wprowadzenie println do kodu metody balance()
w klasie Balance (z poprzednich przykładów) powoduje silną konkurencję wątków.
W sytuacji silnej konkurencji (ale tylko wtedy!) użycie bezpośrednich rygli jest bardziej efektywne od użycia synchronized.
Aby to sprawdzić możemy zastosować następujący - modyfikujący poprzedni przykład - program testowy:
class Balance {
private int number = 0;
private Lock lock = new ReentrantLock();
private boolean useLock; // której metody synchronizacji użyć?
public Balance(boolean ul) {
useLock = ul;
}
public int balance(boolean print) { // parametr print mówi czy wypisać gwiazdkę
if (!useLock) return balanceSynchro(print);
else return balanceLocked(print);
}
// Synchronizacja za pomocą bezposrednich rygli
public int balanceLocked(boolean print) {
try {
lock.lock();
number++;
if (print) System.out.print("*");
number--;
return number;
} finally {
lock.unlock();
}
}
// Użycie synchronized
public synchronized int balanceSynchro(boolean print) {
number++;
if (print) System.out.print("*");
number--;
return number;
}
}
class BalanceThread extends Thread {
private Balance b; // referencja do obiektu klasy Balance
private int count; // liczba pwotórzeń pętli w metodzie run
public BalanceThread(String name, Balance b, int count) {
super(name);
this.b = b;
this.count = count;
start();
}
public void run() {
int wynik = 0;
for (int i = 0; i < count; i++) {
boolean print;
if (i%20 == 0) print = true; // gwiazdkę będziemy drukować co 20 iterację
else print = false;
wynik = b.balance(print);
if (wynik != 0) break;
}
System.out.println("\n"+ Thread.currentThread().getName() +
" konczy z wynikiem " + wynik);
}
}
class BalanceTest {
static ArrayList<String> czasy = new ArrayList<String>(); // wyniki czasowe
public static void test(int tnum, boolean lock) {
// jesli lock jest true będziemy używać bezp. rygli
Balance b = new Balance(lock);
String wynik = lock ? "Lock " : "Synchro ";
String id = lock ? "L" : "S";
wynik += tnum;
// Tworzymy i uruchamiamy wątki
Thread[] thread = new Thread[tnum];
long start = System.currentTimeMillis();
for (int i = 0; i < tnum; i++)
thread[i] = new BalanceThread(id +(i+1), b, 100);
// czekaj na zakończenie wszystkich wątków
try {
for (int i = 0; i < tnum; i++) thread[i].join();
} catch (InterruptedException exc) {
System.exit(1);
}
wynik += " Czas: " + (System.currentTimeMillis() - start);
System.out.println(wynik);
czasy.add(wynik);
// Uduwamy niepotrzebne obiekty-wątki
for (int i = 0; i < thread.length; i++) { thread[i] = null; }
System.gc();
}
public static void main(String[] args) {
//Test synchro
for (int i=100; i<=1000; i+=100) {
test(i, false);
}
// Test Lock
for (int i=100; i<=1000; i+=100) {
test(i, true);
}
for (String msg : czasy) { System.out.println(msg);
}
}
}
Wyniki testu prezentuje poniższy rysunek.
Należy jednak pamiętać, że:
użycie synchronized przy braku silnej konkurencji wątków jest w Javie 1.6 bardziej efektywne,
w Javie 1.6 wewnętrzne mechanizmy synchronizacji zostały zmodyfikowane, tak aby synchronized było
bardziej efektywne przy dużej konkurencji wątków, jednocześnie
mechanizm bezpośrednich rygli poprawiono pod względem efektywności przy
braku silnej konkurencji.
Zatem nie względy efektywności powinny decydować o wyborze mechanizmu podstawowej synchronizacji.
7. Lock czy synchronized?
Użycie synchronized jest w wielu przypadkach łatwiejsze (mniej pisania kodu) i pozwala uniknąć subtelnych pułapek.
Jeśli więć nie potrzebujemy w naszym kodzie dodatkowcyh możliwości
zapewnainych przez bezposrednie rygle typu Lock - używajmy tradycyjnego
synchronizowania przez synchronized.
Użycie bezpośrednich rygli (typu Lock) wymaga trochę więcej kodowania i uwagi.
Ale opłaca się w przypadkach kiedy:
chcemy mieć przerywalne blokady,
piszemy bardziej wysublimowane programy, w których np. nie chcemy blokować wątku, jesli ktoś inny zajął rygiel:
Lock lock;
boolean got = lock.tryLock();
if (got) {
try {
// sekcja krytyczna
} finally {
lock.unlock();
}
} else {
robimy coś innego - może staramy się np. przetwarzać inny zasób
}
chcemy lub musimy inaczej strukturyzować kod (zwalniać rygle w innych blokach niż są zajmowane)
Ten ostatni przypadek ilustruje znany nam przykład aplikacji WEB
(zob. wykład o wzorcu MVC). W tamtym kodzie za synchronizowaliśmy
wykonanie Command. Trzeba jednak rozciągnąć tę synchronizację na
serwlet prezentacji i pobierania paremtrów. Uzycie synchronized
nie pozwala na to, bo są to strukturalnie inne fraghmenty kodu
(umieszczone w innych blokach). Tylko zastosowanie bezposrednich rygli
typu Lock pomoże rozwiązać problem.
Oto zmodyfikowany kod.
// Obsługa zleceń
public void serviceRequest(HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, IOException
{
resp.setContentType("text/html");
// Wywolanie serwletu pobierania parametrów
RequestDispatcher disp = context.getRequestDispatcher(getParamsServ);
disp.include(req,resp);
// Pobranie bieżącej sesji
// i z jej atrybutów - wartości parametrów
// ustalonych przez servlet pobierania parametrów
// Różne informacje o aplikacji (np. nazwy parametrów)
// są wygodnie dostępne poprzez własną klasę BundleInfo
HttpSession ses = req.getSession();
String[] pnames = BundleInfo.getCommandParamNames();
for (int i=0; i<pnames.length; i++) {
String pval = (String) ses.getAttribute("param_"+pnames[i]);
if (pval == null) return; // jeszcze nie ma parametrów
// Ustalenie tych parametrów dla Command
command.setParameter(pnames[i], pval);
}
// Wykonanie działań definiowanych przez Command
// i pobranie wyników
// Ponieważ do serwletu może naraz odwoływać sie wielu klientów
// (wiele watków) - potrzebna jest synchronizacja
Lock mainLock = new ReentrantLock();
mainLock.lock();
// wykonanie
command.execute();
// pobranie wyników
List results = (List) command.getResults();
// Pobranie i zapamiętanie kodu wyniku (dla servletu prezentacji)
ses.setAttribute("StatusCode", new Integer(command.getStatusCode()));
// Wyniki - będą dostępne jako atrybut sesji
ses.setAttribute("Results", results);
ses.setAttribute("Lock", mainLock);
// Wywołanie serwletu prezentacji
disp = context.getRequestDispatcher(presentationServ);
disp.forward(req, resp);
}
Zmknięty rygiel oywieramy dopiero w serwlecie prezentacji:
public class ResultPresent extends HttpServlet {
public void serviceRequest(HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, IOException
{
ServletContext context = getServletContext();
// Włączenie strony generowanej przez serwlet pobierania parametrów
// (formularz)
String getParamsServ = context.getInitParameter("getParamsServ");
RequestDispatcher disp = context.getRequestDispatcher(getParamsServ);
disp.include(req,resp);
// Uzyskanie wyników i wyprowadzenie ich
// Controller po wykonaniu Command zapisał w atrybutach sesji
// - referencje do listy wyników jako atrybut "Results"
// - wartośc kodu wyniku wykonania jako atrybut "StatusCode"
HttpSession ses = req.getSession();
Lock mainLock = (Lock) ses.getAttribute("Lock");
List results = (List) ses.getAttribute("Results");
Integer code = (Integer) ses.getAttribute("StatusCode");
mainLock.unlock(); // otwarcie rygla
PrintWriter out = resp.getWriter();
out.println("<hr>");
// Uzyskanie napisu właściwego dla danego "statusCode"
String msg = BundleInfo.getStatusMsg()[code.intValue()];
out.println("<h2>" + msg + " aha!" + "</h2>");
// Elementy danych wyjściowych (wyników) mogą być
// poprzedzane jakimiś opisami (zdefiniowanymi w ResourceBundle)
String[] dopiski = BundleInfo.getResultDescr();
// Generujemy raport z wyników
out.println("<ul>");
for (Iterator iter = results.iterator(); iter.hasNext(); ) {
out.println("<li>");
int dlen = dopiski.length; // długość tablicy dopisków
Object res = iter.next();
if (res.getClass().isArray()) { // jezeli element wyniku jest tablicą
Object[] res1 = (Object[]) res;
int i;
for (i=0; i < res1.length; i++) {
String dopisek = (i < dlen ? dopiski[i] + " " : "");
out.print(dopisek + res1[i] + " ");
}
if (dlen > res1.length) out.println(dopiski[i]);
}
else { // może nie być tablicą
if (dlen > 0) out.print(dopiski[0] + " ");
out.print(res);
if (dlen > 1) out.println(" " + dopiski[1]);
}
out.println("</li>");
}
out.println("</ul>");
}
//..
}
8. Dodatkowe możliwości rygli
Uczciwe rygle
Lock lock = new ReentrantLock(true); // fairness = true
Najdłużej czekający będą mieli wcześniej dostęp. Koszt efektywnościowy.
Read-Write Locks
Wiele wątków czyta, modyfikacje są rzadsze. Efektywność.
Sposób postępowania:
A. Utworzenie obiektu - rygla
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
Pakiet java.util.concurrent dostarcza alternatywnego wobec wait-notify mechanizmu koordynacji dzialania wątków.
Wprowadzany jest on przez klasę Condition.
Lock lock;
Condition cond1 = lock.newCondition(); // stworzenie warunku w kontekście lock
lock.lock();
//....
try {
cond.await(); // rygiel jest otwierany, wątek przechodzi do stanu WAITING
}
// i wraca do stanu RUNNABLE z zamkniętym ponownie ryglem,
gdy wystąpi jedno ze zdarzeń
lock.unllock(); // - inny wątek wywołał signal lub signalAll
// - wystąpił wyjątek InterruptedException
// - spurious wakeup (mechanizmy systemowe
przerwały wait)
Inny wątek:
lock.lock()
cond.signal() lub cond.signalAll();
lock.unlock();
Różnice wobec wait i notify:
Condition jest obiektem i można z nim robić to co z innymi obiektami (np. przekazywać)
oczywiście jest związany z danym ryglem ale dla każdego rygla może być wiele warunków.
Uwaga na możliwość "spurious wakeup" - konieczność dodatkowych warunków i sprawdzania ich w pętli.
Przykład (modyfikacja kodu Author-Writer):
import java.util.concurrent.locks.*;
class Teksty {
Lock lock = new ReentrantLock();
Condition txtWritten = lock.newCondition();
Condition txtSupplied = lock.newCondition();
String txt = null;
boolean newTxt = false;
// Metoda ustalająca tekst - wywołuje Autor
void setTextToWrite(String s) {
lock.lock();
try {
if (txt != null) {
while (newTxt == true)
txtWritten.await();
}
txt = s;
newTxt = true;
txtSupplied.signal();
} catch (InterruptedException exc) {
} finally {
lock.unlock();
}
}
// Metoda pobrania tekstu - wywołuje Writer
String getTextToWrite() {
lock.lock();
try {
while (newTxt == false)
txtSupplied.await(); // może być Interrupted
newTxt = false;
txtWritten.signal();
return txt;
} catch (InterruptedException exc) {
return null;
} finally {
lock.unlock();
}
}
}
// Klasa "wypisywacza"
class Writer extends Thread {
Teksty txtArea;
Writer(Teksty t) {
txtArea=t;
}
public void run() {
String txt = txtArea.getTextToWrite();
while(txt != null) {
System.out.println("-> " + txt);
txt = txtArea.getTextToWrite();
}
}
}
// Klasa autora
class Author extends Thread {
Teksty txtArea;
Author(Teksty t) {
txtArea=t;
}
public void run() {
String[] s = { "Pies", "Kot", "Zebra", "Lew", "Owca", "Słoń", null };
for (int i=0; i<s.length; i++) {
try { // autor zastanawia się chwilę co napisać
sleep((int)(Math.random() * 1000));
} catch(InterruptedException exc) { }
txtArea.setTextToWrite(s[i]);
}
}
}
// Klasa testująca
public class Koord {
public static void main(String[] args) {
Teksty t = new Teksty();
Thread t1 = new Author(t);
Thread t2 = new Writer(t);
t1.start();
try { Thread.sleep(3000); } catch(Exception exc) {}
t2.start();
}
}
Klasa Condition ma i inne bogate możliwości. Oto ilustrujący je zestaw metod.
void
await()
Bieżący wątek
jest wstrzymywany dopóki nie otrzyma sygnału lub nie zostanie
przerwany (interrupt).
boolean
await(long time,
TimeUnit unit)
Bieżący
watek jest wstrzymywany dopóki nie otrzyma sygnału lub nie
zostanie przerwany (interrupt), albo nie minie podany czas.
long
awaitNanos(long nanosTimeout)
j.w..
void
awaitUninterruptibly()
Tu nie ma możliwości przerwania oczekiwania przez interrupt().
boolean
awaitUntil(Date deadline)
Oczekiwanie
dopóki nei ma signal, interrupt() lub nie minie podana data.
void
signal()
Budzi jedne z czakjacych na warunku wątków.
void
signalAll()
Budzi wsyztskie czekające watki.
10. Synchronizatory wyższego poziomu, blokujące kolejki, konkurencyjne kolekcje i atomiki.
Pakiet java.util.concurrent udostępnia nowe synchronizatory wyższego poziomu:
Semafory (Semaphore),
Bariery (CyclicBarrier),
Zasuwy (CountDownLatch),
Wymienniki (Exchanger).
a także blokujące kolejki (ulatwiające pisanie programów wymagających koordynacji wątków):
ArrayBlockingQueue
LinkedBlockingQueue
Do zestwu kolekcji dodano również kolekcje przygotowane na efektywne działanie w sytuacji dużej konkurencji:
ConcurrentHashMap
ConcurrentLinkedQueue
Pakiet java.util.concurrent.atomic daje natomiast możliwości
wielowątkowo bezpiecznego dzialania na zmiennych typów prostych
bez użcycia synchronizaji. Zagwarantowano to atomistycznośc operacji
modyfikacji zmiennych i dzięki temu można unikac czasowo ksoztownej
synchronizaji.
Proszę zapoznać się z dokumentacją przedstawionych klas.
11. Podsumowanie
Zapoznaliśmy się z nowymi mechanizmami synchronizacji i korrdynacji dzialania wątków.
W szczególności:
zaletamu bezpośrednich rygli typu Lock,
sposobami programowania przy ich użyciu,
warunkami (obiektami klasy Condition) i ich użyciem.
12. Zadania
Zadanie 1
Mamy n zasobów, które są modyfikowane przez m wątków. Modyfikacje trochę
trwają (użyć sleep).
Sprawdzić czy wątek odczytujący dane z zasobów będzie działał bardziej
efektywnie, jeśli zastosować tryLock.
Zadanie 2
Pokazać zastosowanie read/write locków i porównać ich efektywność w stosunku
do zwykłej synchronizacji.
Zadanie 3
Napisać program Author-Writer przy użyciu blokujących kolejek.
Zadanie 4
Porównać efektywnośc współbieżnego dzialanai na zwykłej mapie ( HshMap) i na mapie klasy ConcurrentHashMap.