Wprowadzenie do programowania współbieżnego
Współbieżność oznacza równoległe
wykonywanie różnych zadań. W ramach jednego
programu efekt ten osiągany jest poprzez tworzenie i uruchamianie tzw.
wątków.
Poznamy zatem istotę programowania wielowątkowego w Javie,
sposoby tworzenia,
uruchamiania, wstrzymywania i kończenia wątków oraz zagadnienia
związane
z synchronizacją wątków oraz ich koordynacją. Tematy te będą
przedstawione na podstawowym, elementarnym poziomie.
1. Podstawowe pojęcia: procesy i wątki
Uruchomienie dowolnego program (aplikacji) powoduje stworzenie procesu
w systemie operacyjnym. Dla każdego procesu alokowane są przez system wymagane
przez niego zasoby np. pamięciowe i plikowe.
Proces - to wykonujący się program wraz z dynamicznie przydzielanymi
mu przez system zasobami (np. pamięcią operacyjną, zasobami plikowymi) oraz,
ewentualnie, innymi kontekstami wykonania programu (np. obiektami tworzonymi przez program) .
Systemy wielozadaniowe pozwalają na równoległe (teoretycznie) wykonywanie wielu procesów, z których każdy ma swój kontekst, w tym: swoje zasoby.
W dalszym ciągu będziemy mówić wyłącznie o systemach wielozadaniowych
i wielowątkowych. Należą do nich np. systemy Windows i OS/2, a w różnych
wersjach systemów Unix i Linux wielowątkowość jest symulowana przez biblioteki
wspomagające jądro systemu
W systemach wielozadaniowych i wielowątkowych - jednostką wykonawczą procesu jest wątek.
Każdy proces ma co najmniej jeden wykonujący się wątek, ale może też mieć ich wiele.
Proces "posiada" zasoby i inne konteksty, wykonaniem "zadań" procesu zajmują
się wątki (swego rodzaju podprocesy, wykonujące różne działania w kontekście
jednego procesu).
Wątki działają (teoretycznie) równolegle.
Zatem równoległość działań w ramach procesu (jednego programu) osiągamy przez uruchamianie kilku różnych wątków.
Wątek - to sekwencja działań, która może wykonywać się równolegle z innymi sekwencjami działań w kontekście danego procesu
(programu).
Równoległe działanie procesów w wielozadaniowych systemach operacyjnych obserwujemy
niemal na co dzień. Oto pisząc jakiś tekst w procesorze tekstów możemy równolegle
kompilować program w Javie. Są to dwa różne procesy, związane z dwoma różnymi
programami (procesorem tekstów i kompilatorem Javy).
Przestrzeń adresowa procesu - to zakres dostępnych dla procesu adresów
pamięci operacyjnej. Zakres ten może obejmować wirtualnie całą pamięć operacyjną,
ale dzięki systemowemu zarządzaniu pamięcią nie ma możliwości nakładania
się na siebie fizycznej pamięci przydzielanej różnym procesom. Bardzo często
przestrzenią adresową procesu określa się właśnie nie nakladające się na
siebie dynamicznie przydzielane procesowi przez system obszary pamięci
Każdy
z nich ma inną przestrzeń adresową. Każdy z nich - po otwarciu jakichś plików
(np. pliku z dokumentem i pliku z programem źródłowym) - zawiera deskryptory
(inaczej zwane uchwytami) jednoznacznie identyfikujące otwarte pliki, które
także stanowią przyklad zasobu przydzielonego procesowi.
Procesy mogą wspóldzielić przydzielone im zasoby, ale wymaga to specjalnych zabiegów programisty.
Bardzo łatwo możemy też zaobserwować równoległe działanie watków. Np. w
ramach programu wyszukiwania plików uruchamiany jest wątek przeszukujący
katalogi, a jednocześnie interakcja użytkownika z interfejsem takiego programu
nie jest zablokowana (kontroluje ją inny watek w ramach tego samego procesu
- działającego programu wyszukiwania plików) i w każdej chwili możemy przerwać
działanie wątku przeszukiwania, wybierając jakąś formę opcji "Stop".
Podobnie, ładując pliki z Internetu z poziomu przeglądarki WWW również widzimy
dzialanie wątków. Przeglądarka "nie czeka" na załadowanie pliku - ładowaniem
zajmuje się specjalnie uruchomiony wątek (w ramach procesu - przegladarki),
a my możemy nadal korzystać z jej graficznego interfejsu.
Jak widzimy z wymienionych przykladów, dodatkowych wątków w ramach jednego
programu używa się często (choć nie zawsze) do wykonania czynności, które
zajmują sporo czasu i przez to mogą blokować działanie całego programu. Gdy
zostaną one wyodrębnione jako wątki, działanie aplikacji nie będzie wstrzymywane,
a czasochłonne czynności wykonywane są (praktycznie) równolegle z innymi
(może podstawowymi) działaniami aplikacji, niejako w tle.
Warto w tym miejscu zastanowić się co oznacza równoległość wykonywania wątków?
Przecież w każdym momencie czasu procesor może wykonywać tylko jakąś jedną instrukcję.
Zatem w systemach jednoprocesorowych tak naprawdę w każdym momencie czasu
wykonuje się tylko jeden wątek i nie ma tu "prawdziwej" równoległości.
Wrażenie rownoległości dzialania wątków osiągane jest przez mechanizm przydzielania
czasu procesora poszczególnym wykonującym się wątkom. Każdy wątek uzyskuje
dostęp do procesora na krótki czas (kwant czasu), po czym "oddaje procesor"
innemu wątkowi. Zmiany są tak szybkie, że powstaje wrażenie równoleglości
działania.
Ponieważ Java jest językiem wieloplatformowym, a różne systemy operacyjne
stosują różne mechanizmy udostępniania wątkom czasu procesora, pisząc programy
wielowątkowe w Javie powinniśmy zakladać, że mogą one działac zarówno w środowisku
"współpracy", jak i "konkurencji" (mechanizm wywłaszczania)
Zmiany wątków
"u procesora" mogą dokonywać się wedle dwóch mechanizmów:
- współpracy (cooperative multistasking) - wątek sam decyduje, kiedy oddać czas procesora innym wątkom,
- wywłaszczania (pre-emptive multistasking) - o dostępie
wątków do procesora decyduje systemowy zarządca wątków: przydziela on wątkowi
kwant czasu procesora, po upłynięciu którego odsuwa wątek od procesora i
przydziela kwant czasu procesora innemu wątkowi.
Jak wspomniano wcześniej, proces wykonuje się poprzez wykonanie jego wątków.
Zatem, zwolnienie procesora przez wątek jednego procesu i przydzielenie procesora
wątkowi innego procesu wymaga "przeładowania" kontekstu procesu, bowiem
każdy proces ma swój niezależny kontekst (np. przestrzeń adresową, odniesienia
do otwartych plików).
Podstawowa róznica pomiędzy procesami i wątkami polega na tym, że różne wątki
w ramach jednego procesu mają dostęp do całego kontekstu tego procesu (m.in.
przydzielonych mu zasobów).
Wobec tego zamiana wątków jednego procesu "przy procesorze" jest wykonywana
szybciej niż zamiana procesów (wątków różnych procesów).
Z punktu widzenia programisty wspólny dostęp wszystkich wątków jednego procesu
do kontekstu tego procesu ma zarówno zalety jak i wady.
Zaletą jest możliwość łatwego dostępu do wspólnych danych programu. Wadą
- brak ochrony danych programu przed równoległymi zmianami, dokonywanymi
przez różne wątki, co może prowadzić do niespójności danych, a czego unikanie
wiąże się z koniecznością synchronizacji dzialania wątków.
2. Jak stworzyć i uruchomić nowy wątek?
Przejdźmy teraz do praktyki posługiwania się wątkami w Javie.
Uruchamianiem wątków i zarządzaniem nimi zajmuje się klasa Thread.
Aby uruchomić wątek należy stworzyć obiekt klasy Thread i użyć metody start() wobec tego obiektu.
Ale kod, wykonujący się jako wątek (sekwencja działań, wykonująca się równolegle
z innymi działaniami programu) określany jest przez obiekt klasy implementującej interfejs
Runnable.
Interfejs ten zawiera deklarację metody run(), która przy implementacji musi być zdefiniowana.
Właśnie w metodzie run() zapisujemy kod, który będzie wykonywany jako wątek (równolegle z innymi wątkami programu).
Metoda run() określa co ma robić wątek.
Klasa Thread implementuje interfejs Runnable (podając "pustą" metodę run).
Stąd pierwszy sposób tworzenia i uruchamiania wątku.
Pierwszy sposób tworzenia i uruchamiania wątku
|
- Zdefiniować własną klasę dziedziczącą Thread (np. class Timer extends Thread)
-
Przedefiniować odziedziczoną metodą run(), podając w niej działania, które ma wykonywać wątek
-
Stworzyć obiekt naszej klasy (np. Timer timer = new Timer(...);
-
Wysłać mu komunikat start() (np. timer.start())
|
Niech klasa Timer służy do zliczania czasu (uwaga: nie należy mylić
tej klasy z klasami Timer z pakietu javax.swing oraz Timer z pakietu java.util;
tutaj pod tą nazwą opisujemy własną, prostą klasę - licznik czasu). Klasa
ta może wyglądać tak.
public class Timer extends Thread {
public void run() {
int time = 0;
while (true) {
try {
this.sleep(1000);
} catch(InterruptedException exc) {
System.out.println("Wątek zliczania czasu zoostał przerwany.");
return;
}
time++;
int minutes = time/60;
int sec = time%60;
System.out.println(minutes + ":" + sec);
}
}
}
Kodu zliczającego upływ czasu dostarczyliśmy w metodzie run(). Zmienna time
jest licznikiem sekund. W pętli while usypiamy wątek, wykonujący tę metodę
run(), na 1000 milisekund, czyli 1 sekundę (metoda sleep z klasy Thread),
po czym zwiększamy licznik sekund (zmienna time) i wyprowadzamy informację
o upływie czasu na konsolę.
W trakcie uśpienia wątek odsuwany jest od procesora (kod metody run nie wykonuje się wtedy).
Dzięki temu uzyskujemy pożądany efekt.
Metoda sleep może sygnalizować wyjątek InterruptedException, który powstaje
na skutek przerwania działania wątku. Dlatego musimy obsługiwać ten wyjątek.
Licznik czasu możemy zastosować np. w następującym programie, który wymaga
od użytkownika podania wszystkich stolic (lądowych) sąsiadów Polski.
import javax.swing.*;
public class Quiz {
// Stolice do odgadnięcia
private final String[] CAP = {"Praga", "Bratysława", "Moskwa",
"Berlin", "Kijów", "Wilno", "Mińsk" };
// Czy stolica była już podana ?
private boolean[] entered = new boolean[CAP.length];
public Quiz() {
int n = CAP.length;
JOptionPane.showMessageDialog(null, "Podaj stolice lądowych sąsiadów Polski");
String askMsg = "Wpisz kolejną stolicę:" ;
int count = 0; // ile podano prawidłowych odpowiedzi
// Uruchomienie wątku zliczającego i pokazującego upływający czas
Timer tm = new Timer();
tm.start();
while (count < CAP.length) { // dopóki nie podano wszystkich stolic
String input = JOptionPane.showInputDialog("Odpowiedzi: " + count + '/' + n +
'\n' + askMsg);
if (input == null) break;
if (isOk(input)) count++; // jeżeli ta odpowiedź prawidłowa
}
System.exit(0);
}
// Czy odpowiedź jest prawidłowa i czy jej wcześniej nie podano?
private boolean isOk(String s) {
for (int i=0; i < CAP.length; i++) {
if (s.equalsIgnoreCase(CAP[i]) && !entered[i])
return (entered[i] = true);
}
return false;
}
public static void main(String args[]) {
new Quiz();
}
}
Tutaj w pętli pobieramy dane od użytkownika, dopóki nie wpisze on prawidłowo wszystkich znajdujących się w tablicy CAP
stolic lub nie przerwie wpisywania, wybierając w dialogu Cancel.
Ponieważ wcześniej utworzyliśmy i uruchomiliśmy wątek zliczania czasu:
Timer timer = new Timer();
timer.start();
to nasz program równolegle wykonuje dwa zadania: interakcję z użytkownikiem
(pytania o stolice) oraz wypisywanie na konsoli informacji o upływającym
czasie.
Oczywiście, nic nie stoi na przeszkodzie, by odziedziczyć klasę Thread w
klasie Quiz (pytającej użytkownika o stolice). Przy okazji zobaczymy, że
dwa wątki mogą odwoływać się do wspólnych danych.
Rozważmy modyfikację zadania o stolicach.
Zmodyfikować quiz o stolicach w taki sposób, by kod
programu był umieszczony w jednej klasie. W okienkach komunikatów pokazywać
informację o aktualnym w momencie
otwarcia okienka stanie licznika czasu. Przed zakończeniem programu podać w oknie
komunikatów ile trwało wpisywanie stolic.
Możliwe rozwiązanie.
import javax.swing.*;
public class Quiz1a extends Thread {
private final String[] CAP = {"Praga", "Bratysława", "Moskwa",
"Berlin", "Kijów", "Wilno", "Mińsk" };
private boolean[] entered = new boolean[CAP.length];
private int time = 0; // licznik czasu
public Quiz1a() {
int n = CAP.length;
JOptionPane.showMessageDialog(null, "Podaj stolice sąsiadujących krajów");
String askMsg = "Wpisz kolejną stolicę:" ;
int count = 0;
// Uruchomienie wątku zliczania czasu
start();
while (count < CAP.length) {
String input = JOptionPane.showInputDialog("Odpowiedzi: " + count + '/' + n +
". Czas: " + getTime() + '\n' +
askMsg);
if (input == null) break;
if (isOk(input)) count++;
}
JOptionPane.showMessageDialog(null, "Czas wpisywania: " + getTime());
System.exit(0);
}
// Kod który wykonuje się w odrębnym wątku
public void run() {
while (true) {
try {
this.sleep(1000);
} catch(InterruptedException exc) {
System.out.println("Wątek zliczania czasu został przerwany.");
return;
}
time++;
System.out.println(getTime());
}
}
// Metoda zwracająca bieżący czas w formie min : sek
private String getTime() {
int minutes = time/60;
int sec = time%60;
return minutes + ":" + sec;
}
private boolean isOk(String s) {
for (int i=0; i < CAP.length; i++) {
if (s.equalsIgnoreCase(CAP[i]) && !entered[i])
return (entered[i] = true);
}
return false;
}
public static void main(String args[]) {
new Quiz1a();
}
}
Zwrócmy uwagę: mamy tu również dwa wątki - odpytywania użytkownika oraz zliczania
czasu. Ale kod wykonujący się w wątku zliczania czasu podaliśmy w metodzie
run() w tej samej klasie, która odpowiada za odpytywanie użytkownika. Oba
wątki dzielą między sobą ten sam obiekt - obiekt klasy Quiz1a utworzony w
metodzie main i równolegle odwołują się do jego elementu, określonego przez
zmienną time. Współdzielenie zmiennej uzyskaliśmy bez specjalnych dodatkowych
zabiegów, ale takie równoległe sięganie do jednej zmiennej może powodować
różne komplikacje. W tym przykładzie są one niewidoczne, ale dalej zobaczymy,
że łatwość współdzielenia zasobów (tu: tego samego obiektu) w Javie okupiona
jest - w większości przypadków - koniecznością synchronizowania dostępu wątków do współdzielonych zasobów.
Jak już wiemy, kod wykonywany przez wątek podajemy w metodzie run(). A
metoda run() może być zdefiniowana w dowolnej klasie implementującej interfejs
Runnable.
Klasa Thread dostarcza zaś konstruktora, którego argument jest typu Runnnable.
Konstruktor ten tworzy wątek, który będzie wykonywał kod zapisany w metodzie
run() w klasie obiektu, do którego referencję przekazano wspomnianemu wyżej
konstruktorowi.
Stąd drugi sposób tworzenia i uruchamiania wątków.
Drugi sposób tworzenia i uruchamiania wątku
|
- Zdefiniować klasę implementującą interfejs Runnable (np. class X implements Runnable).
- Dostarczyć w niej definicji metody run (co ma robić wątek).
- Utworzyć obiekt tej klasy (np. X x = new X(); )
- Utworzyć obiekt klasy Thread, przekazując w konstruktorze referencję
do obiektu utworzonego w p.3 (np.Thread thread = new Thread(x);).
- Wywołać na rzecz nowoutworzonego obiektu klasy Thread metodę start ( thread.start();)
|
Oprogramowanie uprzednio omawianego licznika czasu przy użyciu drugiego sposobu
tworzenia i uruchamiania watków wyglądałoby tak:
class Timer implements Runnable {
public void run() {
int time = 0;
while (true) {
try {
Thread.sleep(1000);
} catch(InterruptedException exc) {
System.out.println("Wątek zliczania czasu zoostał przerwany.");
return;
}
time++;
int minutes = time/60;
int sec = time%60;
System.out.println(minutes + ":" + sec);
}
}
}
a utworzenie i uruchomienie wątku zliczającego czas (w innej klasie) w następujący sposób:
Timer timer = new Timer();
Thread thread = new Thread(timer);
thread.start();
lub zwięźlej:
new Thread(new Timer()).start();
Uwaga: tym razem w metodzie run() dla uśpienia wątku na sekundę używamy statycznej
metody sleep z klasy Thread, która usypia bieżący wątek (w tym przypadku
ten, w którym wykonuje się ta metoda run()). Nie mogliśmy użyć metody niestatycznej
(this.sleep()), bo przecież nowa klasa Timer nie dziedziczy już klasy Thread,
zatem wywołanie na rzecz jej obiektów metody sleep() z klasy Thread spowoduje
błąd w kompilacji.
Drugi sposób tworzenia i uruchamiania wątków ma pewne zalety w stosunku do
korzystania wyłącznie z klasy Thread (czyli omówionego wcześniej sposobu
pierwszego):
- niekiedy daje lepsze możliwości separowania kodu (kod odpowiedzialny za pracę wątku może być wyraźnie wyodrębniony w klasie implementującej Runnable).
- a w niektórych okolicznościach - mianowicie, gdy chcemy umieścić
metodę run() w klasie, która dziedziczy jakąś inną klasę - jest jedynym możliwym
sposobem.
Wyobraźmy sobie oto, że w znanym nam już przykładzie z samochodami (zużycie
paliwa w czasie jazdy - zob. klasa Car i materiał o klasach wewnętrznych) nie dysponujemy
możliwością uzycia klasy Timer z pakietu javax.swing.
Sami musimy oprogramować symulację zużycia paliwa.
Jak przedtem, umówimy się, że w każdej sekundzie czasu programu zużywany jest 1 litr paliwa.
Klasa Car dziedziczy klasę Vehicle, zatem nie możemy już odziedziczyć klasy
Thread. Ale możemy implementować interfejs Runnable i w klasie Car dostarczyć
metody run(), która będzie symulować zużycie paliwa.
public class Car extends Vehicle implements Runnable {
private String nrRej;
private int tankCapacity; // pojemność baku
private int fuel; // ile jest paliwa?
public Car(String nr, Person owner, int w, int h, int l,
int weight, int tankCap) {
super(owner, w, h, l, weight);
nrRej = nr;
tankCapacity = tankCap;
}
// Napełnianie baku
public void fill(int amount) {
fuel += amount;
if (fuel > tankCapacity) fuel = tankCapacity;
}
// Start samochodu
public void start() {
if (fuel > 0) {
super.start();
new Thread(this).start(); // uruchamiamy wątek zużycia paliwa
}
else System.out.println("Brak paliwa");
}
// Zatrzymanie samochodu
public void stop() {
super.stop();
}
// Kod, który wykonuje się w odrębnym wątku
// co 1 sek. czasu programu zużywany jest 1 litr paliwa
public void run() {
while(true) {
try {
Thread.sleep(1000);
} catch(InterruptedException exc) { return; }
fuel--;
System.out.println("Paliwo: " + fuel); // śledzimy ile jest paliwa
if (fuel <= 0) break; // jeżeli brak paliwa...
}
System.out.println("Zatrzymanie samochodu z powodu braku paliwa");
stop(); // zatrzymanie samochodu, bo brak paliwa
}
public String toString() {
return "Samochód nr rej " + nrRej + " - " + getState(getState());
}
}
W klasie TestCar1 przetestujemy działanie programu na przykładzie jednego samochodu:
public class TestCar1 {
// Symulacja upływu czasu...
static void delay(int sek) {
while(sek-- > 0) {
try {
Thread.sleep(1000);
} catch (Exception exc) { }
}
}
public static void main(String[] args) {
Car c = new Car("WA1090", new Person("Janek", "0909090"),
100, 100, 100, 100, 50);
c.fill(10); // napełniamy bak
c.start(); // ruszamy ...
System.out.println(c + ""); // co się dzieje z samochodem
delay(12); // niech upłynie 12 sek. jazdy od tego momentu
System.out.println(c + ""); // co się dzieje z samochodem
}
}
Wyniki dzialania programu przedstawia wydruk.
Samochód nr rej WA1090 - JEDZIE
Paliwo: 9
Paliwo: 8
Paliwo: 7
Paliwo: 6
Paliwo: 5
Paliwo: 4
Paliwo: 3
Paliwo: 2
Paliwo: 1
Paliwo: 0
Zatrzymanie samochodu z powodu braku paliwa
Samochód nr rej WA1090 - STOI
Wydaje się, że wszystko jest w porządku. Ale co by się stało gdyby po upływie 3 sekund jazdy zatrzymać samochód?
c.fill(10); // napełniamy bak
c.start(); // ruszamy ...
System.out.println(c + ""); // co się dzieje z samochodem
delay(3); // niech upłyną 3 sek.
c.stop(); // samochód się zatrzymuje...
System.out.println(c + ""); // co się dzieje z samochodem
delay(9); // niech upłynie jeszcze 9 sek. od tego momentu
System.out.println(c + ""); // co się dzieje z samochodem
Wynik nie będzie właściwy, po zatrzymaniu samochodu nadal będzie zużywane paliwo:
Samochód nr rej WA1090 - JEDZIE
Paliwo: 9
Paliwo: 8
Samochód nr rej WA1090 - STOI
Paliwo: 7
Paliwo: 6
Paliwo: 5
Paliwo: 4
Paliwo: 3
Paliwo: 2
Paliwo: 1
Paliwo: 0
Zatrzymanie samochodu z powodu braku paliwa
Nie jest mozliwe przejscie ze stanu STOI do stanu STOI
Samochód nr rej WA1090 - STOI
Dzieje się tak dlatego, że mimo zatrzymania samochodu (metodą stop()),
wątek zużycia paliwa nadal dziala. Tajemniczy komunikat "Nie jest mozliwe
przejscie ze stanu STOI do stanu STOI" pojawia się na skutek użycia metody
stop() (po wyczerpaniu paliwa, w metodzie run()) wobec pojazdu, który stoi
(komunikat jest generowany przez klasę Vehicle).
Niewątpliwie, w momencie zatrzymania samochodu metodą stop() należy zakończyć
działanie wątku zużycia paliwa. Musimy zatem mieć jakiś sposób na kończenie
działania wątków (o czym w następnym punkcie).
Warto jeszcze podkreślić, że kod, który ma wykonywać się jako odrębny wątek możemy dostarczyć
w klasie wewnętrznej.
Przy tworzeniu wątków ad hoc (zwykle tylko raz i na jakąś konkretną potrzebę)
bardzo często posługujemy się anonimowymi klasami wewnętrznymi i to zwykle
lokalnymi.
Na przykład - do zliczania i pokazywania upływu czasu w trakcie jakiejś
interakcji użytkownika z programem, jak w poniższym programie:
import javax.swing.*;
class AnonymousRunnable {
public AnonymousRunnable() {
Runnable runner = new Runnable() {
public void run() {
int time = 0;
while (true) {
try {
Thread.sleep(1000);
} catch(InterruptedException exc) { return; }
System.out.println(time++/60 + " min. " + time%60 + " sek.");
}
}
};
new Thread(runner).start();
String s, out = "";
while ((s = JOptionPane.showInputDialog("Wprowadź jakiś tekst:")) != null)
out += " " + s;
System.out.println(out);
System.exit(0);
}
public static void main(String args[]) {
new AnonymousRunnable();
}
}
Możliwe są różnorakie schematy użycia (lokalnych) anonimowych klas wewnętrznych.
Niektóre standardowe metody Javy mają jako argument referencję do obiekty
typu Runnable i argument ten możemy podawać jako referencję do obiektu anonimowej klasy wewnętrznej, zdefiniowanej
"w miejscu" podania argumentu.
Uwaga:
istnieje jeszcze jeden sposób uruchamiania wątków, mianowicie użycie
tzw. wykonawców (ExecutorService z pakietu java.util.concurrent).
Więcej na ten temat przy omawianiu pakietu java.util.concurrent
(w następnym semestrze).
3. Kończenie pracy wątku
Wątek kończy pracę w sposób naturalny wtedy, gdy zakończy się jego metoda run().
Jeśli chcemy programowo zakończyć pracę wątku, to należy zapewnić w metodzie
run() sprawdzenie warunków zakończenia (ustalanych programowo) i jeśli są
spełnione - spowodować wyjście z run() albo przez "dobiegnięcie do końca",
albo przez return.
Warunki zakończenia mogą być formułowane w postaci
wartości jakiejś zmiennej, które są ustalane przez inne fragmenty kodu programu
(wykonywane w innym wątku).
W klasie Thread znajduje się metoda stop(), która kiedyś służyła do kończenia
działania watków. Stwierdzono jednak, że jej użycie może powodować błędy
w programach i w tej chwili nie należy jej już używać.
W naszym samochodowym przykładzie klasa Vehicle (którą dziedziczy Car) dostarcza
metody getState(), która zwraca aktualny stan pojazdu (m.in. czy jedzie).
Możemy spróbować jej użyć do sprawdzenia warunku kontynuacji działania wątku
zużycia paliwa: jeżeli samochód jedzie, to paliwo jest zużywane, w przeciwnym
razie wątek zużycia paliwa powinien zakończyć działanie.
public void run() {
while(getState() == MOVING) {
try {
Thread.sleep(1000);
} catch(InterruptedException exc) { return; }
fuel--;
System.out.println("Paliwo: " + fuel); // śledzimy ile jest paliwa
if (fuel <= 0) stop(); // jeżeli brak paliwa...zatrzymujemy samochód
}
}
Oto wydruk testu, w którym
- tak jak w końcu poprzedniego podpunktu - po trzech sekundach zatrzymujemy
samochód.
Wynik jest w miarę satysfakcjonujący:
Paliwo: 9
Paliwo: 8
Paliwo: 7
Samochód nr rej WA1090 - STOI
Paliwo: 6
Samochód nr rej WA1090 - STOI
Okazuje się, że samochód zużył 4 litry paliwa, choć symulowany
czas jazdy wynosił ok. 3 sekund. W innym przebiegu programu mogłoby się
okazać, że zużyto trzy (a nie cztery) litry paliwa. O tym dlaczego tak jest
- dowiemy się dalej.
Zwróćmy teraz uwagę na inny problem, który występuje w naszym programie
samochodowym.. Po wprowadzeniu kodu dla wątku zużycia paliwa nasza klasa
Car nie jest odporna na błąd dwukrotnego uruchomienia samochodu (wydania
samochodowi polecenia start dwa razy).
Owszem, w klasie Vehicle zagwarantowaliśmy
brak możliwości przejścia ze stanu MOVING do MOVING (czyli bronimy się przed
błędem, i wypisywany jest komunikat "Nie jest możliwe przejście ze stanu
JEDZIE do stanu JEDZIE"), ale tworzenie wątku zużycia paliwa w metodzie start()
klasy Car nie uwzględnia tego. Napisaliśmy tam po prostu:
new Thread(this).start();
i za każdym razem, kiedy wywołujemy metodę start na rzecz tego samego samochodu
tworzony jest i uruchamiany nowy wątek zużycia paliwa (o ile paliwo jeszcze jest).
Kilka równolegle dzialającyh wątków zużycia paliwa będzie pustoszyć bak!
Możemy się o tym przekonać, modyfikując nieco kod w klasie Car, tak by wiedzieć
jaki wątek zużywa paliwo i próbując np. trzy razy uruchomić ten sam samochód.
Wykorzystamy przy tym:
- konstruktor Thread(Runnable, String), który ma dodatkowy argument
typu String, dzięki czemu każdemu wątkowi możemy nadać unikalną nazwę,
- statyczną metodę currentThread() z klasy Thread, która zwraca referencję do bieżącego wątku (tego, ktory wykonuje ten fragment kodu, w którym znajduje się wywołanie tej metody).
- metodę getName z klasy Thread, która zwraca nazwę watku (w szczególnoście tę nadaną za pomocą konstruktora).
class Car {
...
int tnr = 1; // numer wątku
// Start samochodu
public void start() {
if (fuel > 0) {
super.start();
new Thread(this, "Nr " + tnr++).start();
}
else System.out.println("Brak paliwa");
}
...
public void run() {
Thread cThread = Thread.currentThread();
while(getState() == MOVING ) {
try {
Thread.sleep(1000);
} catch(InterruptedException exc) { return; }
fuel--;
System.out.println("Paliwo zużywa wątek: " + cThread.getName() +
", pozostało paliwa: " + fuel);
if (fuel <= 0) stop();
}
}
}
class Test {
....
public static void main(String[] args) {
Car c = new Car(...);
c.fill(10);
c.start();
c.start();
c.start();
delay(10);
...
}
}
Wynik działania programu pokazuje, że zostały uruchomione trzy wątki zużycia
paliwa, co powoduje trzykrotnie szybsze jego wyczerpanie:
Nie jest mozliwe przejscie ze stanu JEDZIE do stanu JEDZIE // to są komunikaty z klasy Vehicle
Nie jest mozliwe przejscie ze stanu JEDZIE do stanu JEDZIE
Paliwo zużywa wątek: Nr 2, pozostało paliwa: 9
Paliwo zużywa wątek: Nr 1, pozostało paliwa: 8
Paliwo zużywa wątek: Nr 3, pozostało paliwa: 7
Paliwo zużywa wątek: Nr 2, pozostało paliwa: 6
Paliwo zużywa wątek: Nr 1, pozostało paliwa: 5
Paliwo zużywa wątek: Nr 3, pozostało paliwa: 4
Paliwo zużywa wątek: Nr 2, pozostało paliwa: 3
Paliwo zużywa wątek: Nr 1, pozostało paliwa: 2
Paliwo zużywa wątek: Nr 3, pozostało paliwa: 1
Paliwo zużywa wątek: Nr 1, pozostało paliwa: 0
Niewątpliwie musimy zabezpieczyć się przed wielokrotnym uruchamianiem wątków
zużycia paliwa dla tego samego samochodu.
Postaramy sie zatem bezpośrednio w klasie Car wprowadzić zmienną, która będzie
kontrolować działanie wątku zużycia paliwa. Wygodna okaże się tu zmienna,
która reprezentuje sam wątek zużycia paliwa. Jej wykorzystanie pozwala na
proste tworzenie i uruchamianie wątku (wtedy, gdy wątek jeszcze nie istnieje),
a także łatwe kończenie pracy wątku.
Jest to metoda bardzo często stosowana w programach, gdzie potrzebne jest
wielokrotne uruchamianie i kończenie jakiegoś watku, a jednocześnie trzeba
zagwarantować, by w każdym momencie działał tylko jeden wątek, wykonujący
kod podanej metody run().
Wprowadzimy zatem do klasy Car definicję zmiennej fuelConsumeThread, która
będzie referencją do wątku zużycia paliwa. Inicjalnie będzie ona miała wartość
null. Każde wywolanie metody start() wobec samochodu będzie sprawdzać, czy
zmienna fuelConsumeThread ma wartośc null. Jeśli tak, to wątek nie istnieje
i trzeba go utworzyć i uruchomić. Jeśli nie - wątek już działa i nie należy
nic robić.
W metodzie run() możemy pobrać referencję do bieżącego wątku (tego, który
aktualnie wykonuje tę metodę run). Metoda run powinna zakończyć działanie,
kiedy referencja do bieżącego wątku nie będzie równa zapamiętanej w zmiennej
fuelConsumeThread referencji do wątku zużycia paliwa.
Jeśli więc zmiennej fuelConsumeThread nadamy (w wątku głównym programu) wartość null, to uzyskamy trzy efekty:
- wątek zużycia paliwa zakończy działanie,
- ten obiekt-wątek (który wlaśnie zakończył działanie) będzie usunięty z pamięci przez odśmiecacza,
- kolejne wywołanie metody start() wobec samochodu stworzy i uruchomi nowy wątek zużycia paliwa.
public class Car extends Vehicle implements Runnable {
...
private Thread fuelConsumeThread;
...
// Start samochodu
public void start() {
if (fuel > 0) {
super.start();
if (fuelConsumeThread == null) {
fuelConsumeThread = new Thread(this);
fuelConsumeThread.start();
}
}
else System.out.println("Brak benzyny");
}
// Zatrzymanie samochodu
public void stop() {
fuelConsumeThread = null;
super.stop();
}
// Zużycie paliwa - wykonuje się jako wątek
public void run() {
Thread cThread = Thread.currentThread();
while(cThread == fuelConsumeThread ) {
try {
Thread.sleep(1000);
} catch(InterruptedException exc) { return; }
fuel--;
System.out.println("Pozostało paliwa: " + fuel);
if (fuel <= 0) stop();
}
}
....
}
Zwróćmy uwagę, że wątek główny (w którym wykonują się metody start
oraz stop, wywoływane na rzecz samochodu) odwołuje się (właśnie w tych metodach)
do zmiennej fuelConsumeThread. Do tej samej zmiennej odwołuje się wątek zużycia
paliwa. Dwa wątki równolegle operują na tym samym elemencie obiektu.
Wydawałaby się, że nie ma w tym nic zlego. Jednak ze względu na efektywność
działania, w Javie wątki operują na kopiach zmiennych, uzgadniając ich wartości
z oryginałami tylko w niektórych punktach wykonania programu, tzw. punktach
synchronizacji. Zatem metoda stop() lub start()
z klasy Car może zmienić wartość swojej kopii zmiennej fuelConsumeThread,
a metoda run() - wykonująca się w wątku zużycia paliwa będzie sprawdzać wartość
swojej kopii tej zmiennej i nie będzie dostatecznie wcześniej "widziała"
dokonanej zmiany.
Aby zabezpieczyć się przed tą konkretną sytuacją można zadeklarować współdzieloną zmienną ze specyfikatorem volatile
(co oznacza, że wartości oryginału i kopii zmiennej będą uzgadniane przy
każdym odwołaniu do niej) lub też zastosować mechanizmy synchronizacji (inaczej
zwane wykluczaniem), bo właśnie w punktach synchronizacji na pewno następuje
uzgadnainie kopii z oryginałem. Synchronizacja dzialania watków ma jednak
dużo szersze znaczenie i pora teraz na jej omówienie.
Uwaga:
jeśli wątek może być kontrolowane przez ExecutorService z pakietu
java.util.concurrent, to właściwym sposobem kończenia pracy wątku jest
użycie metody inetrrupt(), a w metodzie run(0 sprawdzanie flagi
interrupted. Parę słów o tym w dalszych punktach, a więcej dowiemy się
przy omawianiu pakietu java.util.concurrent.
4. Synchronizacja wątków
Rozważmy następujący 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 watpliwości, że jakiekolwiek wielokrotne wywoływanie
metody balance() na rzecz dowolnego obiektu klasy Balance zawsze zwróci wartość
0.
Otóż, w świecie programowania współbieżnego nie jest to wcale takie oczywiste!
Więcej: wynik różny od 0 może pojawiać się nader często!
Przekonajmy się o tym poprzez wielokrotne wywoływanie metody balance() na rzecz tego samego obiektu w kilku różnych wątkach.
Każdy z wątków będziemy tworzyć i uruchamiać poprzez stworzenie obiektu poniższej
klasy BalanceThread, dziedziczącej Thread, i wywołanie na jego rzecz metody
start(). Przy tworzeniu nazwiemy każdy z wątków (parametr name konstruktora).
Wielokrotne wywołania metody balance() zapiszemy w pętli w metodzie run().
Obiekt na rzecz którego jest wywoływana metoda oraz liczbę powtórzeń pętli
przekażemy jako dwa pozostałe argumenty konstruktora.
Tuż przed zakończeniem metody run() pokażemy jaki był wynik ostatniego odwołania do metody balance().
class BalanceThread extends Thread {
private Balance b; // referencja do obiektu klasy Balance
private int count; // liczba powtórzeń pętli w metodzie run
public BalanceThread(String name, Balance b, int count) {
super(name);
this.b = b;
this.count = count;
start();
}
public void run() {
int wynik = 0;
// W pętli wielokrotnie wywołujemy metodę balance()
// na rzecz obiektu b klasy Balance.
// Jeżeli wynik metody jest różny od zera - przerywamy działanie pętli
for (int i = 0; i < count; i++) {
wynik = b.balance();
if (wynik != 0) break;
}
// Pokazujemy wartość zmiennej wynik na wyjściu z metody run()
System.out.println(Thread.currentThread().getName() +
" konczy z wynikiem " + wynik);
}
}
W klasie testującej stworzymy obiekt klasy Balance, po czym stworzymy i uruchomimy
podaną przez użytkownika liczbę wątków, które za pomocą metody run() z klasy
BalanceThread będą równolegle operować na tym obiekcie wielokrotnie wywołując
na jego rzecz metodę balance() z klasy Balance.
class BalanceTest {
public static void main(String[] args) {
int tnum = Integer.parseInt(args[0]); // liczba wątków
int count = Integer.parseInt(args[1]); // liczba powtórzeń pętli w run()
// Tworzymy obiekt klasy balance
Balance b = new Balance();
// Tworzymy i uruchamiamy wątki
Thread[] thread = new Thread[tnum]; // tablica wątków
for (int i = 0; i < tnum; i++)
thread[i] = new BalanceThread("W"+(i+1), b, count);
// czekaj na zakończenie wszystkich wątków
try {
for (int i = 0; i < tnum; i++) thread[i].join();
} catch (InterruptedException exc) {
System.exit(1);
}
System.out.println("Koniec programu");
}
}
Uwaga: metoda join z klasy Thread powoduje oczekiwanie na zakończenie
wątku, na rzecz któego została wywołana. Oczekiwanie może być przerwane,
gdy wątek został przerwany przez inny wątek - wtedy wystąpi wyjątek InterruptedException.
Uruchamiając aplikację z podanymi jako argumenty liczbą wątkow = 2 oraz liczbą
powtorzeń pętli w metodzie run() = 100000, nader często zyskamy intuicyjnie
oczekiwany wynik (W1 konczy z wynikiem 0, W2 konczy z wynikem 0). Może się
jednak zdarzyć wynik inny! Zwiększenie liczby wątków i liczby powtórzeń pętli
prawie na pewno szybko pokaże nam, że niektóre wątki zakończą działanie z wynikem
różnym od 0.
Na przyklad, przy liczbie wątkow = 5 i liczbie powtórzeń pętli = 1000000, możemy raz uzyskac następujący wynik:
W2 konczy z wynikiem 0
W3 konczy z wynikiem 0
W4 konczy z wynikiem 0
W1 konczy z wynikiem 0
W5 konczy z wynikiem 0
a za chwilę, przy ponowym uruchomieniu z tymi samymi argumentami:
W1 konczy z wynikiem 1
W3 konczy z wynikiem 1
W2 konczy z wynikiem 1
W5 konczy z wynikiem 0
W4 konczy z wynikiem 0
Testowanie programów wielowątkowych jest trudne, bowiem możemy wiele razy
otrzymać wyniki, które wydają się świadczyć o poprawności programu, a przy
kolejnym uruchomieniu okaże się, że wynik jest nieprawidłowy. Wyniki uruchamiania
programów wielowątkowych mogą być także różne na różnych platformach systemowych.
Powstaje oczywiste pytanie: jak to się dzieje, że w powyższym przykładowym
programie uzyskujemy wyniki, których - wydaje się na podstawie analizy kodu
metody balance() - nie sposób uzyskać?
Otóż, wszystkie wykonujące (tę samą) metodę run() wątki odwołują się do tego
samego obiektu klasy Balance (w programie oznaczanego przez b). Mówimy: współdzielą
obiekt.
Obiekt ten ma jeden element - odpowiadający zmiennej number zdefiniowanej jako pole klasy Balance.
Wywolywana przez wątki na rzecz tego obiektu metoda balance() zwiększa a następnie zmniejsza wartość tej zmiennej.
Wyobraźmy sobie, że działają dwa wątki. Jeden z nich uzyskuje czas procesora
i rozpoczyna wykonanie metody balance(). Po zwiększeniu o 1 zmiennej number
zostaje wywlaszczony (zmienna number ma teraz wartość 1). Czas procesora
przydzielany jest drugiemu wątkowi. Drugi wątek rozpoczyna wykonanie metody
balance() i zwiększa o 1 wartość zmiennej number (zmienna number ma teraz
wartość 2), po czym zostaje wywlaszczony, a czas procesora przydzielony zostaje
wątkowi pierwszemu, który kontynuuje wykonanie metody run() od miejsca, w
którym odebrano mu czas procesora. Zmniejsza on teraz zmienną number i zwraca
wynik, który równy jest 1. Po zakończeniu wątku pierwszego, pracę kontynuuje
wątek drugi. Po zmniejszeniu zmiennej number zwraca wynik 0.
Obrazuje to poniższy rysunek.
Komentarze do rysunku:
- Punkt a - wątek W1, po zwiększeniu zmiennej number zostaje wywłaszczony. Zaczyna działanie wątek W2
- Punkt b - wątek W2 po zwiększeniu zmiennej number zostaje wywłaszony.
Pracę kontunuuje wątek W1 (mając za wartość zmiennej number 2)
- Punkt c - po zakończeniu wątku W1 (który zmniejszył number i zwrócił
1), wątek W2 kontunnuje pracę: zmniejsza number i zwraca wynik 0.
Jest to sytuacja, która może się zdarzyć, ale nie musi. Wszystko zależy od
tego czy i kiedy (w jakim momecie wykonania metody run()) systemowy zarządca
wątków wywlaszczy wykonujący się aktualnie wątek. A z tym jest bardzo różnie
w zależności od platformy systemowej czy aktualnego obciążenia procesora.
Zawsze jednak musimy się liczyć z tym, że wątki operujące na wspóldzielonych
zmiennych mogą być wywłaszczone w trakcie operacji (nawet pojedyńczej) i
wobec tego stan wspóldzielonej zmiennej może okazać się niespójny.
Aby uniknąć równoczesnego działania wątków na tym samym obiekcie (co w sposób
nieprzewidywalny ukształtować może jego stany) stosuje się rygle.
Każdy egzamplarz klasy Object i jej podklas posiada rygiel.
Ryglowanie obiektów dokonuje się automatycznie i sterowane jest słowem kluczowym synchronized.
Po części ze względu na to slowo kluczowe mówimy o synchronizowaniu fragmentów kodu, wykonywanych przez wątki.
Synchronizowane mogą być metody i bloki.
Metoda synchronizowana oznaczana jest w deklaracji słowem synchronized:
synchronized void metoda() {
...
}
UWAGA: słowo kluczowe synchronized nie jest częścią sygnatury metody!
Zatem przedefiniowanie metody synchronizowanej może być synchronizowane albo nie
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ć inen przyczyny zwolneinia
rygla, o których będzie mowa w podrozdziale o stanach wątków).
W naszym przykładowym programie (z klasami Balance i BalanceThread) dla zapewnienia
prawidłowego działania (otrzymywania z metody balance() wyników zawsze równych
0) można zatem zdefiniować metodę balance() w klasie Balance jako synchronizowaną.
class Balance {
private int number = 0;
synchronized public int balance() {
number++;
number--;
return number;
}
}
Wtedy każde wywołanie metody balance() przez jakiś wątek na rzecz obiektu,
oznaczanego (w klasie testującej) przez b spowoduje zajęcie obiektu (zamknięcie
rygla). Inne wątki starające się równolegle działać na tym samym obiekcie
za pomoca metody balance() będą czekać na zakończenie jej wykonania przez
wątek, który zajął obiekt. Zatem każdy wątek wykona obie operacje ++ i --
bez ingerencji ze strony innych wątków i wynik metody balance() zawsze będzie
równy 0.
Synchronizowanie wątków jest czasowo kosztowne. Łatwo się o tym przekonać
porównując czasy wykonania omawianego programu testującego z i bez synchronizacji
metody balance().
Ta czasochłonnośc wynika z operacji zamykania i otwierania rygli (które to operacje nie
tylko polegają na ustawianiu jakichś flag, ale wiążą się z prowadzeniem i
zmianami kolejek oczekującyh na zaryglowanym obiekcie watków). Gdy - w naszym
przykładzie - metoda balance() jest synchronizowana, to operacje te - w każdym
wątku - wykonywane są tyle razy ile wynosi liczba powtórzeń pętli w metodzie
run(). Przy dużej liczbie powtórzeń pętli nasz program może działać bardzo
długo.
Pewne sposoby strukturyzacji programów wielowątkowych pozwalają
w ogóle unikać synchronizacji przy jednoczesnym wykluczaniu możliwości powstawania
niespójnych stanów obiektów. Należą do nich m.in.:
- użycie klas niezmiennych (immutables), bowiem obiekty niezmienne (właśnie
przez swą niezmienność) nie stwarzają problemu spójności stanów przy dostępie
z wielu wątków,
- użycie kodów wielobieżnych (reentrant); kody wielobieżne
są bezpieczne w użyciu wielowątkowym; przykładowo, kod który używa wyłącznie
zmiennych lokalnych i nie odnosi się do pól klasy jest wielobieżny i nie
wymaga synchronizacji,
- czasami można użyć zmiennych ze specyfikatorem volatile
(o którym była już mowa); generalnie dostęp do takich zmiennych nie musi
być synchronizowany dla operacji atomistycznych (wykonywanych "w jednym takcie"),
ale to czy dana operacja jest atomistyczna zależy od architektury systemowej
i implementacji JVM.
Jak widać, unikanie synchronizacji (przy zapewnieniu wielowątkowej spójności)
może wymagać dużego wysiłku koncepcyjnego przy projektowaniu aplikacji i wyborze sposobów kodowania.
Są jednak też i pewne proste, niejako oczywiste zasady, których należy przestrzegać.
Niewatpliwie synchronizacja nie ma żadnego sensu, jeśli wątki jedynie odczytują
jakieś dane. Nie ma też powodów, aby synchronizować kody, o których wiadomo,
że będą wykonywane tylko w jednym wątku.
W aplikacjach, które powinny
działać naprawdę efektywnie (np. w czasie realnym) należy w ogóle unikać
synchronizacji na poziomie metod.
Właśnie - po części w kontekście ew. możliwości poprawy efektywności działania
programów, ale nie tylko - warto zwrócić uwagę na ogólniejszy (od metod synchronizowanych)
sposób synchronizowania wątków - mianowicie - bloki synchronizowane.
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
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 których będzie mowa w podrozdziale o stanach wątków).
Bloki synchronizowane są ogólniejsze od metod synchronizowanych, bowiem zapis każdej synchronizowanej metody:
synchronized void metoda() {
// kod
}
jest równoważny zapisowi:
void metoda() {
synchronized(this) {
// kod
}
}
natomiast za pomocą instrukcji synchronized możemy również synchronizować dowolne fragmenty kodu wewnątrz metod.
W naszym przykładzie "bilansowym" synchronizację wątków możemy uzyskać używając
instrukcji synchronized w metodzie run() klasy BalanceThread.
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;
synchronized(b) {
for (int i = 0; i < count; i++) {
wynik = b.balance();
if (wynik != 0) break;
}
System.out.println(Thread.currentThread().getName() +
" konczy z wynikiem " + wynik);
}
}
}
Łatwo się przekonać, że to rozwiązanie (w tym konkretnym przypadku) jest
lepsze, bowiem ryglowanie obiektu b odbywa się tylko tyle razy ile jest wątków,
a nie przy każdym powtórzeniu pętli for.
Należy podkreślić, że konstruktory nie mogą być synchronizowane (nie
możemy poprzedzić defnicji konstruktora słowem kluczowym synchronized).
Konstrukcja synchronized(this) w konstruktorze także nie zadziała. Możliwym
rozwiązaniem synchronizacji kodu konstruktora jest wprowadzenie semafora
w klasie i ryglowanie kodu na tym semaforze np.:
class SynchroConstr {
Object lock = new Object(); // semafor
SynchroConstr() {
synchronized(lock) {
// ... kod konstruktora
}
}
}
Alternatywnie można użyć synchronizacji na obiekcie-klasie (obiekcie klasy Class, który reprezentuje daną klasę) np.
synchronized(SynchroConstr.class) {
...
}
Uwaga: literał nazwa_klasy.class oznacza obiekt-klasę nazwa_klasy.
Na takich obiektach-klasach są zresztą synchronizowane metody statyczne:
class Klasa {
public static synchronized void jakasMetoda() { .. }
}
jest równoważne:
class Klasa {
public static void jakasMetoda() {
synchronized(Klasa.class) {
// ... kod metody
}
}
}
Warto podkreślić, że zawsze powinniśmy mieć pełną świadomość jaki obiekt jest ryglowany. Rozważmy przykład.
class Liczba {
static double n;
synchronized static void set(double x) { n = x; } // 1
synchronized double get() { return x; } // 2
}
Tutaj metoda set jest synchronizowana na obiekcie Liczba.class (obiekcie-klasie),
a metoda get - na obiekcie this (obiekcie klasy Liczba, na rzecz którego
jest wołana). To są dwa różne obiekty, dwa różne rygle. Zatem nie uzyskujemy
wykluczania w dostępie do pola n!
W takich przypadkach powinniśmy użyć synchronizacji na obiekcie Liczba.class
(wprowadzając do metody get blok synchronizowany), albo realizować dostęp
do pól statycznych wyłącznie za pomocą statycznych metod synchronizowanych.
Podobna sytuacja dotyczy metod definiowanych w klasach wewnętrznych np.
class Outer {
double n;
class Inner {
synchronized void metoda() {
// ... dostęp do pola n klasy otaczającej
}
}
}
nie synchronizuje dostępu do pola n (bo mamy tu synchronizację na obiekcie klasy wewnętrznej, a nie otaczającej).
W tym przypadku należy napisać tak:
class Inner {
void metoda() {
synchronized(Outer.this) {
// dostęp do pola n klasy otaczającej
}
}
Kończąc omawianie synchronizacji warto poświęcić chwilę na pojęciowe, trochę
terminologiczne, podsumowanie (jest to również istotne ze względu na różną
terminologię stosowaną w literaturze).
Synchronizacja jest mechanizmem, który zapewnia, że kilka wykonujących się watków:
- nie będzie równocześnie działać na tym samym obiekcie,
- nie będzie równocześnie wykonywać tego samego kodu.
Synchronizację wątków uzyskuje się za pomocą obiektów, które wykluczają równoczesny
dostęp wątków do zasobów i/lub równoczesne wykonanie przez wątki tego samego
kodu. Takie obiekty nazywają się ogólnie muteksami (od ang. mutual-exclusion semaphore). W Javie rolę muteksów pełnią rygle (ang lock).
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 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.
5. Koordynacja wątków
Ryglowanie (użycie synchronized) służy do zapobiegania niepożądanej interakcji wątków.
Nie jest ono jednak wystarczającym środkiem dla zapewnienia współdziałania wątków.
Przykład:
Dwa wątki Author i Writer mogą odwoływać się do tego samego obiekty typu Teksty.
Author podrzuca teksty, zapisywane w polu txt, Writer wypisuje je na konsoli.
Do ustalania tekstów służy metoda setTextToWrite (wywołuje ją Author), teksty
do zapisu odczytywane są przez Writera za pomocą metody getTextToWrite i
wypisywane na konsoli.
Ponieważ metody te mogą być wywołane równocześnie (z różnych wątków) i operują
na polu tego samego obiektu, winny być synchronizowane.
Ale tu ważna jest również kolejność i koordynacja działań obu wątków.
Chodzi o to, by Writer zapisywał tylko raz to co poda Autor, a Autor nie
podawał nic nowego, dopóki Writer nie zapisze poprzednio podanego tekstu.
Skoordynowanie interakcji pomiędzy wątkami uzyskuje się za pomocą metod klasy Object:
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 ktorego 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) koordynacji:
class X {
int n;
boolean ready = false;
....
synchronized int get() {
try {
while(!ready)
wait();
} catch (InterruptedException exc) { .. }
ready = false;
return n;
}
synchronized void put(int i) {
n = i;
ready = true;
notify();
}
}
Uwaga: metoda wait() może sygnalizować wyjątek InterruptedException (w
przypadku, gdy nastąpiło zewnętrzne przerwanie oczekiwania na skutek użycia
w innym wątku metody interrupt()). Wyjątek ten musimy obsługiwać.
Wyobraźmy sobie, że działają tu dwa wątki - ustalający wartość n za pomocą put i pobierający wartość n za pomocą get.
Wątek pobierający musi czekać, aż wątek ustalający ustali wartość n (wait).
Ustalenie wartości powoduje dwie zmiany: warunek "wartość gotowa" staje się true, a oczekiwanie jest przerywane przez notify.
Zwrócmy 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 (sa to kolejne
elmenty tablicy napisów). Wątek-Writer pobiera ustalony tekst i wypisuje
na konsoli. Zakończenie pracy Autor sygnalizuje poprzez podanie tekstu =
null.
// Klasa dla ustalania i pobierania tekstów
class Teksty {
String txt = null;
boolean newTxt = false;
// Metoda ustalająca tekst - wywołuje Autor
synchronized void setTextToWrite(String s) {
while (newTxt == true) {
try {
wait();
} catch(InterruptedException exc) {}
}
txt = s;
newTxt = true;
notifyAll();
}
// Metoda pobrania tekstu - wywołuje Writer
synchronized String getTextToWrite() {
while (newTxt == false) {
try {
wait();
} catch(InterruptedException exc) {}
}
newTxt = false;
notifyAll();
return txt;
}
}
// Klasa "wypisywacza"
class Writer extends Thread {
Teksty txtArea;
Writer(Teksty t) {
txtArea=t;
}
public void run() {
String txt = txtArea.getTextToWrite();
while(txt != null) {
System.out.println("-> " + txt);
txt = txtArea.getTextToWrite();
}
}
}
// Klasa autora
class Author extends Thread {
Teksty txtArea;
Author(Teksty t) {
txtArea=t;
}
public void run() {
String[] s = { "Pies", "Kot", "Zebra", "Lew", "Owca", "Słoń", null };
for (int i=0; i<s.length; i++) {
try { // autor zastanawia się chwilę co napisać
sleep((int)(Math.random() * 1000));
} catch(InterruptedException exc) { }
txtArea.setTextToWrite(s[i]);
}
}
}
// Klasa testująca
public class Koord {
public static void main(String[] args) {
Teksty t = new Teksty();
Thread t1 = new Author(t);
Thread t2 = new Writer(t);
t1.start();
t2.start();
}
}
Warto skompilować program i prześledzić jego dzialanie.
Następnie, po usunięciu konstrukcji while(newTxt == ... ) oraz wait() i notifyAll()
w metodach setTxtToWrite i getTxtToWrite skompilowac oraz uruchomić program
ponownie i przekonac się, że sama synchronizacja wątków (pozostawiamy obie
metody jako synchronizowane) nie wystarcza dla zapewienia własciwej 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óżnienuiu
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".
6. Stany wątków
Wątek może znajdować się w czterech stanach: New Thread, Runnable, NotRunnable, Dead.
Stan New Thread powstaje w momecie stworzenia obiektu-wątku.
Do stanu Runnable wątek przechodzi po wywołaniu metody start(), która z kolei wywołuje run().
Stan Runnable niekoniecznie oznacza, że wątek jest aktualnie wykonywany.
Raczej jest to potencjalna gotowość do działania, a to czy system operacyjny
akurat przydzieli temu wątkowi czas procesora zależy od wielu czynników (jaki
jeszcze inne wątki konkurują o czas procesora, jaki jest schemat zarządzania
wątkami, jakie są ich priorytety itp.).
Zatem wątki, będące w stanie Runnable można podzielić na aktualnie wykonujące się (będące w stanie Running) i aktualnie gotowe do wykonania (ale czekające na przydzielenie czasu procesora).
Wykonujący się wątek (będący w stanie Running) może dobrowolnie oddać czas procesora za pomocą wywolania metody yield()
. Przy tym nie następuje jednak zwolnienie rygla. Wątek pozostaje w stanie
Runnable, ale czeka na ponowne przydzielenie czasu procesora.
Do stanu NotRunnable wątek przechodzi na skutek:
- wywołania metody sleep (usypiającej wątek). Powrót do stanu Runnable
- po upływie podanego czasu (uwaga: jeśli uśpiony wątek zajął jakiś obiekt,
to rygiel nie jest zwalniany przy przesunięciu wątku do stanu NotRunnable,
co może powodować blokowanie działania programu),
- albo wywołania metody wait, po którym następuje zwolnienie rygla. Powrót
do stanu Runnable po tym jak inny wątek użyje na rzecz tego samego obiektu,
na którym synchronizowany jest czekający wątek, metody notify lub notifyAll,
- albo blokowania na operacjach wejścia-wyjścia. Powrót do stanu Runnable: po zakończeniu operacji,
- albo blokowanie na obiekcie z zamkniętym ryglem.
Wątek kończy działanie na skutek zakończenia metody run() i przechodzi do stanu Dead.
Mimo, iż nadal może istnieć obiekt oznaczający wątek, ponowne użycie wobec
niego metody start() jest niedozwolone (w Javie wątki nie są "restartowalne").
Aby ponownie uruchomić wątek, należy stworzyć nowy obiekt i zastosować metodę start().
7. Wstrzymywanie i przerywanie wątków
Działanie wątku może zostać wstrzymane i - później - wznowione. Niegdyś
służyły temu metody suspend() i resume() z klasy Thread.
Okazało się, że są one niespójne i mogą powodować zakleszczanie wątków.
Obecnie do wstrzymywania i przywracania działania wątków proponuje się mechanizm wait-notify.
Można użyć następującego prostego schematu.
Przykładowy schemat wstrzymywania i wznawiania wątków
|
Wstrzymaniem wątku dyryguje zmienna typu boolean o nazwie np. suspended
W metodzie run() zapisujemy:
while (warunek_działania_wątku) {
try {
synchronized(this) {
while (suspended) // warunek wstrzymania wątku
wait();
}
} catch(InterruptedException exc) { return; }
// ... praca wątku.
}
Inny wątek - gdy chce wstrzymać działanie tego wątku - ustala suspended = true.
Wznowienie osiągane jest przez suspended = false i notify() na rzecz tego wątku.
|
Użycie metody interrupt() z klasy Thread powoduje ustalenie statusu
wątku jako "przerwany" i - w niektórych sytuacjach - wygenerowanie wyjątku InterruptedException.
Nie należy mylić użycia metody interrupt() - opisywanej zresztą zawsze nieco
dwuznacznie jako metody "przerywającej wątek" - z zakończeniem działania
wątku.
Ogólnie, metoda ta ustala jedynie status wątku jako przerwany.
Kiedy wątek ze statusem "przerwany" jest (lub zostanie) zablokowany na wywołaniu metod:
jego status zmieniany jest na "nieprzerwany" i generowany jest wyjątek InterruptedException.
Jak juz widzieliśmy, przy wywołaniu tych metod wyjątek ten musimy obsługiwać.
W obsłudze wyjątku możemy zdecydować czy wątek ma być zakończony czy nie.
Przykładowy program pokazuje w jaki sposób możemy wstrzymywać, wznawiać,
kończyć i przerywać pracę wątku. Działania te próbujemy na wątku, który wypisuje
ciąg liczb rzeczywistych (od zera, zwiększanych co 1). Zwróćmy uwagę na użycie
metody isAlive() (zwraca true, jesli wątek nie jest w stanie Dead) i isInterrupted()
(zwraca true jeśli wątek ma status "przerwany").
Warto też przetestować program i zobaczyć jakie są konsekwencje różnych poleceń
wydawanych przez użytkownika z dialogów wejściowych.
import javax.swing.*;
class SomeThread extends Thread {
volatile boolean stopped = false;
volatile boolean suspended = false;
public void run() {
double d = 0;
while(!stopped) {
try {
synchronized(this) {
while (suspended) wait();
}
} catch (InterruptedException exc) {
System.out.println("Obsluga przerwania watku w stanie wait");
System.out.println("Co powie isInterrupted() ?: "+ isInterrupted());
}
if (suspended) System.out.println("Watek wstrzymany na wartosci " + d);
else System.out.println(++d);
}
}
public void stopThread() {
stopped = true;
}
public void suspendThread() {
suspended = true;
}
public void resumeThread() {
suspended = false;
synchronized(this) {
notify();
}
}
}
public class ActionsOnThread {
public static void main(String args[]) {
String msg = "I = interrupt\n" +
"E = end\n" +
"S = suspend\n" +
"R = resume\n" +
"N = new start";
SomeThread t = new SomeThread();
t.start();
String cmd;
while ((cmd = JOptionPane.showInputDialog(msg)) != null) {
char c = cmd.charAt(0);
switch (c) {
case 'I' : t.interrupt(); break;
case 'E' : t.stopThread(); break;
case 'S' : t.suspendThread(); break;
case 'R' : t.resumeThread(); break;
case 'N' : if (t.isAlive())
JOptionPane.showMessageDialog(null, "Thread alive!!!");
else {
t = new SomeThread();
t.start();
}
break;
default : break;
}
JOptionPane.showMessageDialog(null,
"Command " + cmd + " executed.\n" +
"Thread alive ? " + (t.isAlive() ? "Y\n" : "N\n") +
"Thread interrupted ? " + (t.isInterrupted() ? "Y\n" : "N")
);
}
System.exit(0);
}
}
Rysunki pokazują działanie programu: wubór opcji I (przerwanie wątku), po
której wątek nadal działa, natomiast po następującym po tym wyborze S (suspend-wstrzymanie
wątku) sygnalizowany jest wyjątek InterruptedException (o czym świadczy widoczny
komunkat), wątek jest jednak wstrzymany, bowiem w obsłudze wyjątku nie zmieniamy
tego stanu.
Warto zauważyć, że flaga przerwania wątku jest już "zdjęta", a komunikat
mówiący o tym, że wątek jest przerwany - błędny. Dzieje się tak dlatego,
że wykonanie metody isInterrupted() w wątku głównym nie jest dobrze zsynchronizowane.
Jako ćwiczenie można zaproponować modyfikację przdstawionego kodu w taki
sposób, by komunikat pokazywany w okienku był zgodny z komunikatem wypisywany
na konsoli przy obsłudze wyjątku InterruptedException.
8. Priorytety wątków
Priorytet wątku jest liczbą całkowitą. Wątki o wyższym priorytecie
powinny
od systemowego zarządcy wątków uzyskiwać częstszy dostęp do procesora
Priorytety wątków odgrywają rolę przy ich szeregowaniu.
Szeregowaniem wątków nazywa się ustalenie kolejności wykonania (dostępu do procesora) wielu wątków na maszynie jednoprocesorowej
JVM stosuje deterministyczny algorytm szeregowania wątków zwany szeregowaniem
stało-priorytetowym i bazującym na priorytetach wątków.
Gdy uruchamiamy nasz program, tworzony jest i uruchamiany jego wątek główny i w nim wykonywana jest metoda main(...). Wątek ten ma priorytet oznaczony stałą Thread.NORMAL_PRIORITY (obecnie 5).
W wątku głównym możemy tworzyć inne wątki, a w tych innych - jeszcze inne. Gdy tworzony jest nowy watek - uzyskuje on ten sam priorytet, co wątek w którym został stworzony.
Po utworzeniu wątku możemy zmienić jego priorytet za pomocą metody setPriority(int).
Dostępny zakres priorytetów obejmuje liczby z przedziału Thread.MIN_PRIORITY... Thread.MAX_PRIORITY (obecnie 1-10).
Po utworzeniu wątku, wątek znajduje się w stanie NEW THREAD i nie jest jeszcze gotowy do wykonania. Dopiero użycie metody start() na rzecz wątku przeprowadza go w stan gotowości do wykonania - zwany RUNNABLE. Zob. stany wątków.
Przypomnijmy: bycie w stanie RUNNABLE nie znaczy, że wątek się wykonuje. Może
on czekać w kolejce innych wątków na przydzielenie czasu procesora przez
zarządcę wątków
Gdy w danym momencie czasu wiele wątków gotowych jest do wykonania (czeka
w kolejce na czas procesora) zarządca wątków wybiera wątek z najwyższym priorytetem
i jemu przydziela czas.
Gdy czekają wątki o tym samym priorytecie - wybierany jest jeden z nich.
Wybrany wątek wykonuje się dopóki:
- nie zwolni procesora na skutek uśpienia (metoda sleep), dobrowolnego
oddania czasu (metoda yield()), lub innych przyczyn (o których mowa była przy omawianiu
stanów wątków),
- wątek o wyższym priorytecie nie będzie gotowy do wykonania,
- nie upłynie przydzielony mu kwant czasu procesora (jeśli system operacyjny zapewnia wywłaszczanie po upływie kwantu czasu).
Następnie zarządca wątków przydziela czas kolejnemu wątkowi z kolejki oczekujących na czas procesora. I tak "w koło Macieju".
Możemy zatem spróbować zapewnić właściwe (cosekundowe) zużycie paliwa ustalając
dla wątku zużycia paliwa priorytet wyższy od priorytetu wątku z którego zostaje
on uruchomiony.
Do ustalania priorytetów watków sluży metoda klasy Thread
setPriority(int prior)
gdzie prior - priorytet wątku
Priorytet danego wątku możemy odczytać natomiast za pomocą metody getPriority().
W różnych systemach operacyjnych są różne liczby priorytetów. Priorytety
Javy są odwzorywywane na priorytety uwzględniane przez systemowego zarządcę
watków, co może prowadzić do innego od oczekiwanego (na poziomie programu
napisanego w Javie) szeregowania wątków.
Wobec tego:
W programch wieloplatformowych należy unikać stosowania priorytetów wątków.
Ustalanie priorytetów może być natomiast pomocne przy szczegółowym dostrajaniu
działania programu na danej platformie systemowej
9. Demony i grupy
Niejaki dla porządku (bo nie są to zagadnienia
pierwszoplanowe) kilka słów należy powiedzieć o specjalnym rodzaju wątków
- demonach, oraz o łączeniu wątków w grupy.
Demony - to wątki, których przeznaczeniem jest wykonywanie swoistych prac systemowych lub
pomocniczych w tle. Podstawową różnicą pomiędzy zwykłymi wątkami i wątkami-demonami
jest to, iż program nie może skończyć działania w sposób naturalny o ile
działa jeszcze jakiś zwykły wątek (tzw. watek uzytkownika, user thread),
natomiast wątki-demony automatycznie kończą działanie wraz z zakończeniem
programu i nie wstrzymują zamknięcia aplikacji,
Nieraz mogliśmy zaobserwować w przykładach niniejszej książki, iż program
musiał być kończony przez wywołanie metody exit. Działo się tak w sytuacjach
gdy używaliśmy dialogów JOptionPane. Przyczyna: w sytuacjach, gdy odwołujemy
się do klas GUI (AWT i Swing), oprócz głównego wątku uruchamiany jest wątek
obsługi zdarzeń. To on właśnie - jako zwykły, nie demon - blokuje zakończenie
programu.
Do ustalenia rodzaju wątku służy metoda setDaemon(boolean), która musi być wywołana przed uruchomieniem wątku,
Wątki są łączone w grupy. Każdy wątek należy do jakiejś grupy. Możemy
też tworzyć nowe grupy wątków i dołączac do tych grup nowotworzone watki.
Służy do tego klasa ThreadGroup (oraz argument konstruktora klasy Thread).
Możemy uzyskiwać wątki należące do grupy i wykonywać różne opercaje "od razu"
na wszystkich wątkach grupy.
10. Zliczanie czasu i zadania czasowe
Dotąd w tym rozdziale - przy okazji wielowątkowych programów przykładowych
- dostarczaliśmy własnych sposobów na zliczanie czasu. Za każdym razem wymagało
to pisania jakiegoś kodu. Tymczasem w standardzie Javy istnieją sposoby już
gotowe. Jeden z nich - w postaci użycia kalsy javax.swing.Timer - poznaliśmy
już przy omawianiu klas wewnętrznych. Jest też drugi sposób, bardziej elastyczny
od "zegara" swingowego - mianowicie użycie klas Timer i TimerTask z pakietu
java.util.
W tym podrozdziale zasygnalizujemy tylko ich istnienie, po szczególy odsyłając do dokumentacji.
Klasa Timer służy do uruchamiania zadań, określanych przez obiekty klasy
TimerTask w określonym czasie i/lub z określoną częstotliwością.
Zdefiniowanie zadania do wykonania polega na odziedziczeniu klasy TimerTask
i dostarczeniu kodu do wykonania w metodzie run(). Następnie za pomoca metody
schedule klasy Timer możemy zlecić, by kod ten został wykonany albo o określonym
czasie, albo by jego wykonanie powtarzało sie z określoną częstostliwością,
albo połaczyć obie te możliwości: rozpoczęcie zadania w określonym czasie
i powtarzanie go z określoną częstotliwością. Odwołanie zadania do wykonania
lub zakończenie wykonującego się zadania (jeśli jest to zadanie powtarzające
się) uzyskujemy za pomocą wywołania na jego rzecz metody cancel().
Obrazuje to poniższy przykładowy program.
import java.util.*;
// Klasa definiująca zadanie do wykonania
// tu będzie to wypisywanie komunikatów
class Message extends TimerTask {
private String msg; // komunikat
private boolean showDate; // czy pokazać datę
public Message(String s, boolean show) {
msg = s;
showDate = show;
}
// Ten kod będzie wykonywany przez Timer
// według charakterystyk czasowych podanych w metodzie schedule
public void run() {
String msg1 = msg;
if (showDate) msg1 = "Jest " + new Date() + '\n' + msg;
System.out.println(msg1);
}
}
// Test
public class Timer1 {
public static void main(String[] args) {
// Utworzenie zadań
Message msgTask1 = new Message("Przypominam o podlaniu kwiatów!", true),
msgTask2 = new Message("I zakończ ten program!", false);
// Czas rozpoczęcia drugiegi zadania:
// za trzy sekundy od teraz
long taskTime2 = System.currentTimeMillis() + 3000;
// Utworzenie timera
Timer tm = new Timer();
// Zlecenie pierwszego zadania do wykonania
// Ma się zacząć JUŻ - podano 0 jako moment startu
// i powtarzać się co 2 sekundy
tm.schedule(msgTask1, 0, 2000);
// Zlecenie drugiego zadania do wykonania
// Zacznie się w momencie określanym przez datę
// - utworzony obiekt Date; tu - zgodnie z czasem timeTask2
// ale mogłoby to być np. 11 lipca o 9 rano
tm.schedule(msgTask2, new Date(taskTime2), 2000);
// Po twierdzącej odpowiedzi na to pytanie ...
int rc = javax.swing.JOptionPane.showConfirmDialog(null,
"Czy kwiaty podlane?");
// ... kończymy działanie zadania msgTask1
// do czego służy metoda cancel():
if (rc == 0) msgTask1.cancel();
// zadanie 2 nadal działa
javax.swing.JOptionPane.showMessageDialog(null, "Kończyć trzeba");
// teraz kończymy wszystko
System.exit(0);
}
}
Działanie programu ilustruje rysunek.
Klasa Timer może okazać się bardzo użyteczna: dostarcza różnych strategii
określania częstotliwości powtórzeń (np. czy - przy niedokładnym przecież
pomiarze czasu środkami programowymi - preferować zachowanie stałych odstępów
czasowych między wykonaniem zadania czy też może dążyć do utrzymania sumarycznego
czasu wykonania, wynikającego z liczby powtórzeń). Jest także dobrze skalowalna:
działanie Timera jest takie samo przy małej jak i dużej (idącej w tysiące)
liczbie kontrolowanych przez niego wątków-zadań,
11. Pułapki programowania wspólbieżnego
Problem: samolubne i zagłodzone wątki
Co się stanie, jeśli w metodzie run(), definiującej działanie wątku zawrzemy pętlę wykonującą sie kilka milionów razy?
Czy inny wątek - w trakcie działania pętli - będzie miał szansę dostępu do procesora?
Może nie mieć, gdy ten długo wykonujący się wątek zawłaszczy czas procesora.
Wątek zawłaszczający nazywa się "asmolubnym", a ten, ktory nigdy nie ma szansy dostępu do procesora "zagłodzonym".
Możemy manipulować priorytetami, albo liczyć na systemowe wywłaszczanie...
Ale to może nic nie dać (np. jeśli system nie podtrzymuje wywłaszczania).
Zagłodzenie jakiegoś wątku może pojawić się także na skutek określonej konfiguracji
działania kilku nawet z pozoru bezpiecznych wątków.
Rozwiązanie
Zapewnić dobrowolne wywłaszczanie się wątków:
- za pomocą wywołania metody sleep(...). Uśpienie wątku, nawet na krótko
(np. 1/10 sek.) daje szanse dostępu do procesora innym wątkom.
- poprzez zastosowanie metody yield() (np. po wykonaniu jakiejś częsci
zadania wątku), która to metoda powoduje oddanie czasu procesora innemu oczekującemu
wątkowi, który ma wyższy lub taki sam priorytet
Problem: zakleszczenie
Sytuacja: wątek A wywołuje na rzecz obiektu x synchronizowaną metodę mx. Obiekt x zostaje zaryglowany.
W tym samym czasie wątek B wywołuje na rzecz obiektu y synchronizowaną metodę my. Obiekt y zostaje zaryglowany.
W metodzie mx występuje wywołanie jakiejś innej metody sybchronizowanej na
rzecz obiektu y. Ale obiekt jest zaryglowany i wątek A czeka na jego odryglowanie.
W metodzie my występuje wywołanie jakiejś metody synchronizowanej na rzecz
obiektu x. Ale obiekt x jest zaryglowany i wątek B czeka.
Oba wątki będą czekać w nieskończoność.
Rozwiązanie
Odpowiednie projektowanie programów.
Informacje na ten temat zawiera np. książka Gruźlewski, Weiss, "Programowanie
współbieżne i rozproszone w przykładach i zadaniach", WNT 1993.