Wykład 9-wątki java.pdf

(277 KB) Pobierz
13.05.2018
Wykład 9
<
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;
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
1/24
13.05.2018
Wykład 9
// 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.
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
2/24
13.05.2018
Wykład 9
        ...
        }
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
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
4/24
13.05.2018
Wykład 9
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.
https://edu.pjwstk.edu.pl/wyklady/zap/scb/W9/W9.htm
5/24
Zgłoś jeśli naruszono regulamin