Wykład 9 wątki java

background image

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;

background image

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.

background image
background image

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

background image

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.

background image

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();
}
}

background image

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:

background image

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;
}

background image

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 ).

background image

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

background image

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 {

background image

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.

background image

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 {

background image

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ął

background image

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);
}

background image

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);
}

background image

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:

background image

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);

background image

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>");
}

background image

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

background image

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"

background image

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.

background image

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

background image

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.

 


Wyszukiwarka

Podobne podstrony:
wyklad5.cpp, JAVA jest językiem programowania obiektowego
java wyklad3 0
java wyklad 4 0
Java podstawy jezyka wyklad1
Java wykłady1, Java wykłady:
Java watki
java wyklad2
Java 07 Watki GUI
Java wykład 3, Java wykład 2
java wyklad

więcej podobnych podstron