13.05.2018
Wykład 9
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
1/24
<
9. Współbieżna Java: synchronizacja i koordynacja
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;
13.05.2018
Wykład 9
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
2/24
// 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.
13.05.2018
Wykład 9
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
4/24
...
}
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 instrukcji synchronized 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
13.05.2018
Wykład 9
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
5/24
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.
13.05.2018
Wykład 9
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
6/24
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();
}
}
13.05.2018
Wykład 9
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
7/24
}
// 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:
13.05.2018
Wykład 9
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
8/24
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;
}
13.05.2018
Wykład 9
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
9/24
}
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 ).
13.05.2018
Wykład 9
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
10/24
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
13.05.2018
Wykład 9
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
11/24
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 {
13.05.2018
Wykład 9
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
12/24
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 napisać:
public String get(int i) {
lock.lock();
try {
return txt[i];
} finally {
lock.unlock();
}
}
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.
13.05.2018
Wykład 9
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
13/24
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 {
13.05.2018
Wykład 9
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
14/24
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.
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 {
lock.unlock();
}
System.out.println("Task 1 stopped");
}
};
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:
Task 1 begins
Task 1 interrupted
Exception in thread "pool-1-thread-2" java.lang.IllegalMonitorStateException
....
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ął
13.05.2018
Wykład 9
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
15/24
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);
}
13.05.2018
Wykład 9
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
16/24
// 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);
}
13.05.2018
Wykład 9
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
17/24
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:
13.05.2018
Wykład 9
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
18/24
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);
13.05.2018
Wykład 9
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
19/24
// 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>");
}
13.05.2018
Wykład 9
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
20/24
//..
}
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();
B. Pobranie read/write locków
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();
C. Użycie readLocków wszędzie tam gdzie odczytujemy zasoby
public double getData() {
readLock.lock();
try { . . . }
finally { readLock.unlock(); }
}
D. Użycie writeLocków wszędzie tam, gdzie modyfikujemy zasoby.
public void modifyData(. . .) {
writeLock.lock();
try { . . . }
finally { writeLock.unlock(); }
}
9. Warunki
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
13.05.2018
Wykład 9
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
21/24
} // 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"
13.05.2018
Wykład 9
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
22/24
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.
13.05.2018
Wykład 9
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
23/24
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
13.05.2018
Wykład 9
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
24/24
Porównać efektywnośc współbieżnego dzialanai na zwykłej mapie ( HshMap) i na mapie klasy ConcurrentHashMap.