Sunteți pe pagina 1din 7

Rezolvarea excluderii reciproce în Java

Pentru a realiza excluderea reciprocă, vom folosi facilitatea din limbajul Java
de a declara o metodă cu modificatorul synchronized .
Regula este următoarea: dacă mai multe fire de executare apelează metode
declarate cu modificatorul synchronized prin intermediul aceluiaşi obiect Ob
având ca tip clasa C ce conţine metodele respective, atunci la fiecare moment de timp
cel mult un fir de executare este în curs de a executa o astfel de metodă. Dacă un fir
de executare este în curs de a executa o metodă sincronizată, el o va executa până la
capăt; orice alt fir de executare ce invocă o astfel de metodă va fi blocat. Când un fir
termină de executat o metodă sincronizată, atunci unul dintre firele de executare
blocate va fi deblocat şi va începe efectiv invocarea metodei dorite.
Este folosită următoarea terminologie:
- obiectul Ob se numeşte monitor;
- spunem că monitorul este liber dacă nu este în curs de executare nici o
metodă sincronizată a sa; în caz contrar, adică dacă (exact) o metodă sincronizată a sa
este în curs de executare, spunem că monitorul este ocupat;
- dacă un fir de executare invocă, prin intermediul monitorului Ob, o metodă
sincronizată a clasei C şi monitorul este ocupat, firul va fi blocat în mulţimea (de fire
blocate) de aşteptare asociată monitorului.
- dacă un fir este în curs de a executa, prin intermediul obiectului Ob, o metodă
sincronizată a clasei C, spunem că el deţine controlul (exclusiv) asupra monitorului;
- când firul de mai sus termină de executat metoda sincronizată, el pierde
controlul asupra monitorului. Dacă mulţimea asociată monitorului este nevidă, se
alege "la întâmplare" (în funcţie de implementarea concretă) un fir din această
mulţime şi acesta trece la executarea metodei pe care acesta a încercat (fără succes) să
o invoce. Acum monitorul este din nou ocupat şi noul fir este cel care deţine controlul
asupra monitorului.
Atragem atenţia că definiţia de mai sus a monitorului este valabilă doar pentru
Java. Alte definiţii întâlnite în literatura de specialitate presupun existenţa unor
anumite operaţii asupra monitoarelor, impun o disciplină de coadă pentru mulţimea de
aşteptare asociată monitorului etc.
Mai menţionăm faptul că Java permite nu numai sincronizarea unei întregi
metode, dar şi sincronizarea doar a unui bloc (delimitând astfel mai strict porţiunea
"critică" din cod, în scopul creşterii vitezei programului). Nu ne vom ocupa de acest
aspect, mai ales că mecanismele de bază nu diferă.

Alte primitive de sincronizare. Problema Producător - Consumator


Principalele primitive (metode) de sincronizare puse la dispoziţie de Java sunt
următoarele metode publice ale clasei rădăcină Object :
final void wait()
final void wait(long t)
final void notify()
final void notifyAll()

Înainte de a trece la prezentarea lor, specificăm că ele:


- pot lansa excepţia IllegalMonitorStateException dacă firul curent
nu deţine controlul asupra monitorului reprezentat de obiectul curent;
- pot fi invocate doar din interiorul unei metode sincronizate sau a unui bloc
sincronizat.

• Metodele wait
Fie F firul de executare din care este invocată una dintre aceste metode şi fie
Ob obiectul (monitorul) curent. Executarea metodei suspendă (blochează) firul F,
adăugându-l mulţimii ataşate monitorului Ob; în acelaşi timp monitorul este "eliberat",
fiind gata să ofere controlul asupra sa unuia dintre (eventualele) fire din mulţimea sa
de aşteptare.
Firul F rămâne în mulţimea de aşteptare a monitorului până când este
îndeplinită una dintre următoarele condiţii:
- un alt fir de executare invocă metoda notify prin intermediul obiectului Ob
şi firul F este cel ales pentru a prelua controlul asupra monitorului;
- un alt fir de executare invocă metoda notifyAll prin intermediul obiectului
Ob;
- numai pentru metoda wait cu un parametru: a expirat timpul de aşteptare t
măsurat în în milisecunde şi specificat ca argument.
În continuare firul F aşteaptă ca monitorul să devină liber (invocarea
metodelor notify şi notifyAll nu eliberează monitorul), după care el revine în
starea dinaintea invocării lui wait; cu alte cuvinte, doar în acest moment se încheie
executarea metodei wait.

• Metodele notify şi notifyAll

Metoda notify "trezeşte" unul dintre eventualele fire din mulţimea de


aşteptare asociată monitorului care a invocat metoda. Acesta devine candidat la a
prelua controlul asupra monitorului conform mecanismului descris mai sus.
Metoda notifyAll diferă de notify prin aceea că "trezeşte" toate
eventualele fire din mulţimea de aşteptare asociată monitorului care a invocat metoda.

• Recapitulare. Noţiunea de monitor

Prin monitor înţelegem un obiect Ob de tipul unei clase C care conţine metode
sincronizate. Scopul principal este asigurarea excluderii reciproce: dacă mai multe fire
de executare invocă metode sincronizate prin intermediul lui Ob, atunci la fiecare
moment de timp cel mult un fir este în curs de a executa o astfel de metodă.
Monitorul este liber dacă nu este în curs de executare nici o metodă a sa; în caz
contrar, spunem că monitorul este ocupat şi că firul care execută o metodă a sa deţine
controlul asupra monitorului.

Monitorului Ob îi sunt ataşate două mulţimi:


MOb : mulţimea de fire blocate la monitorul Ob;
WOb : mulţimea de aşteptare asociată monitorului Ob.
Dacă un fir de executare invocă o metodă sincronizată a clasei C şi monitorul
este ocupat, firul va fi blocat în mulţimea MOb.
Dacă un fir de executare ce deţine controlul asupra monitorului execută o
metodă wait, atunci el este trecut în mulţimea WOb şi pierde controlul asupra
monitorului.
Când un fir ce deţine controlul asupra monitorului execută:
- metoda notify : atunci este ales un fir din WOb şi acest fir este trecut în MOb;
- metoda notifyAll : atunci toate firele din WOb sunt trecute în MOb.
Reamintim că executarea metodelor de mai sus nu implică cedarea controlului asupra
monitorului, aşa cum se întâmplă în cazul executării unei metodei wait.

Un fir pierde controlul asupra monitorului în următoarele situaţii şi cu următoarele


consecinţe:
- termină de executat metoda: este ales unul dintre firele blocate din MOb şi acesta
este eliminat din MOb şi capătă controlul asupra monitorului;
- invocă metoda wait() : firul curent este blocat în WOb şi se continuă ca în cazul
precedent;
- invocă metoda wait(long) : nu vom trata acest caz.

Observaţie. Din cele de mai sus rezultă că executarea metodei notify nu


desemnează neapărat firul ales ca fiind cel care preia controlul asupra monitorului la pirderea
controlului: oricare dintre firele din mulţimea MOb (cu conţinutul de la momentul pierderii
controlului), va putea prelua controlul. Acelaşi lucru este valabil şi pentru metoda
notifyAll.

• Problema Producător – Consumator

Reamintim enunţul problemei: Un producător şi un consumator îşi desfăşoară


în acelaşi timp activitatea, folosind în comun o bandă de lungime lung. Producătorul
produce câte un obiect şi îl plasează la un capăt al benzii. Consumatorul ia câte un
obiect de la celălalt capăt al benzii şi îl consumă.
Dificultatea problemei constă în faptul că producătorul şi consumatorul
pun/iau obiecte pe/de pe bandă în ritmuri imprevizibile, ceea ce poate conduce la
următoarele situaţii limită:
- producătorul încearcă să pună un obiect în banda plină;
- consumatorul încearcă să ia un obiect de pe banda vidă.
Exemplu. Prezentăm programul, urmat de explicaţii.
import java.util.*;
class Time extends Random {
int delay() { return (int) (100.0f * nextFloat()); }
}
class Banda {
int lung=3, n, p, u=lung-1;
char[] coada = new char[lung];
static Time ObT = new Time();
synchronized char ia() {
char ch;
while (n == 0) {
try { wait(); }
catch (InterruptedException e) {}
}
ch = coada[p]; p = (p+1)%lung; n--;
IO.write(" I" + ch); notify(); return ch;
}
synchronized void pune(char ch) {
while (n == lung) {
try { wait(); }
catch (InterruptedException e) {}
}
u = (u+1)%lung; coada[u] = ch; n++;
IO.write(" P"+ch); notify();
}
}
class Prod implements Runnable {
Banda b;
Prod(Banda bb) { b = bb; }
public void run() {
char ch;
for (ch='a'; ch<='z'; ch++) {
b.pune(ch);
try { Thread.sleep(Banda.ObT.delay()); }
catch (InterruptedException e) {}
}
}
}
class Cons implements Runnable {
Banda b;
Cons(Banda bb) { b = bb; }
public void run() {
char ch;
do {
ch = b.ia();
try { Thread.sleep(Banda.ObT.delay()); }
catch (InterruptedException e) {}
}
while (ch != 'z');
}
}
class PC {
public static void main(String[] w) {
Banda b = new Banda();
Prod P = new Prod(b); Cons C = new Cons(b);
Thread FirP = new Thread(P); FirP.start();
Thread FirC = new Thread(C); FirC.start();
try { FirP.join(); FirC.join(); }
catch (InterruptedException e) {}
}
}

Clasa Time furnizează un alt mod de accentuare a nedeterminismului: metoda


delay întoarce un număr întreg cuprins între 0 şi 100. Această modalitate va fi
folosită pentru a întârzia un timp aleator operaţiile de punere şi luare pe/de pe bandă.
În metoda delay a fost folosită funcţia nextFloat a clasei Random din pachetul
java.util, funcţie ce întoarce o valoare de tip float din intervalul [0,1).
Clasa Banda ţine evidenţa cozii corespunzătoare benzii: vectorul coada are
lungimea lung, primul său element este pe poziţia p, ultimul pe poziţia u, iar numărul
de elemente de pe bandă este n. Metoda ia realizează extragerea unui element de pe
bandă, iar metoda pune corespunde introducerii unui nou element pe bandă;
elementele sunt caractere. Metodele ia şi pune sunt declarate cu modificatorul
synchronized .
Obiectul P de tipul Prod, clasă ce implementează interfaţa Runnable,
lansează un fir de executare corespunzător producătorului; sunt produse literele mici
ale alfabetului şi se încearcă plasarea lor pe bandă prin invocarea metodei pune.
Obiectul C de tipul Cons, clasă ce implementează şi ea interfaţa Runnable,
lansează un fir de executare corespunzător consumatorului; prin invocarea repetată a
metodei ia se încearcă preluarea unui caracter de pe bandă, până când s-a obţinut
caracterul 'z'.
Cele două fire de executare folosesc acelaşi obiect b de tipul Banda pentru
invocarea metodelor ia şi pune.
Sincronizarea este asigurată prin folosirea metodelor wait şi notify.
Observaţie. Clişeul standard de folosire a metodei wait este următorul:
while ( ! condiţie ) wait();
şi a fost folosit ca atare în programul de mai sus deşi, prin natura problemei concrete,
puteam înlocui while cu if. Motivul este că, în general, deblocarea nu garantează
îndeplinirea condiţiei.

Ceasul deşteptător al lui Hoare


Problema ceasului deşteptător a fost enunţată în anul 1974 de C.A.R. Hoare şi
este prezentată în continuare. Mai multe persoane, numerotate cu 1,2,...,n au
următorul tabiet: persoana i doreşte să se trezească din i în i unităţi de timp pentru a
întreprinde o foarte scurtă activitate, după care se culcă la loc. Se cere să se simuleze
aceste activităţi.

Fiecărei persoane i îi asociem un fir p[i] de tipul unei clase Pers, fir care
urmăreşte trezirea persoanei din i în i unităţi de timp; mai precis, este apelată metoda
CeasLocal a unei clase Alarma, metodă care simulează ceasul personal al persoanei
(în sensul că identifică momentul când trebuie să-şi trezească "stăpânul" şi semnalează
acest lucru printr-o operaţie de scriere).
Pentru ţinerea evidenţei timpului este creat un fir m de tipul unei clase
Mecanism, care este executat concurent cu firele asociate persoanelor. Firul invocă în
mod repetat metoda tictac a aceleiaşi clase Alarma după scurgerea unei unităţi de
timp.
Metodele tictac şi CeasLocal folosesc un câmp al clasei Alarma (o
resursă comună), deci trebuie executate sub excludere reciprocă. De aceea ele sunt
sincronizate şi vor fi invocate prin intermediul aceluiaşi obiect (monitor) al de tipul
Alarma.
În clasa Alarma vom folosi câmpul acum corespunzător timpului curent;
câmpul este iniţializat cu 0 şi valoarea sa va fi mărită cu o unitate după scurgerea unei
unităţi de timp (egală aici cu 500 milisecunde).
Metoda tictac actualizează la fiecare invocare a sa câmpul acum, semnalează
acest lucru (prin tipărirea valorii variabilei) şi autorizează reluarea executării celorlalte
metode prin notify().
Metoda CeasLocal asociată persoanei i întreprinde următoarele acţiuni:
- stabileşte momentul când urmează să fie prima dată trezită persoana i;
valoarea acestui moment (adică acum+i) este memorată în variabila locală timp;
- atâta timp cât valoarea variabilei acum (care ţine evidenţa timpului) nu este
egală cu timp, firul p[i] care a invocat metoda execută în mod repetat secvenţa de
operaţii:
wait(); notify();
prin care întrerupe, pentru o durată (mult mai scurtă decât o unitate de timp), convenţia
de excludere reciprocă, pentru a ceda dreptul la executare şi celorlalte fire (persoane)
care aşteaptă acest lucru;
- când acum=timp, va fi afişat numărul persoanei.
class Alarma {
int acum;
synchronized public void CeasLocal(int i) {
int timp = 0;
for(int j=0;j<10;j++) {
timp = acum+i;
while (acum<timp) {
try { wait(); }
catch(InterruptedException e) { }
notify();
}
IO.write(i+" ");
}
}
synchronized public void tictac() {
acum++; IO.write("\n" + acum + ":\t");
notify();
}
}

class Mecanism extends Thread {


Alarma al;
public void run() {
for(int j=1;j<=100;j++) {
try { sleep(500); }
catch(InterruptedException e ) { }
al.tictac();
}
}
}

class Pers extends Thread {


int i; static Alarma al;
Pers(int i) { this.i = i; }
public void run() { al.CeasLocal(i); }
}

class Hoare {
public static void main(String[] s) throws Exception {
Alarma al = new Alarma();
Mecanism m = new Mecanism(); m.al = al;
Pers.al = al;
Pers[] p = new Pers[10];
for (int i=0; i<10; i++) p[i] = new Pers(i+1);
for (int i=0; i<10; i++) p[i].start();
m.start();
}
}

Iniţial, executarea firului m este amânată o unitate de timp prin sleep.


Persoanele îşi încep activitatea şi sunt toate blocate în mulţimea Mal asociată
monitorului al.
Mai general, presupunem că aceasta este situaţia înainte de momentul de timp t.
La momentul de timp t, executarea firului m este reluată. El invocă metoda
tictac, care actualizează timpul şi execută notify(). La terminarea metodei
tictac, una dintre persoane este deblocată din Mal, execută notify()(prin care altă
persoană este "trezită") şi reia ciclul while; atunci:
- fie trece de while, scrie numărul său de ordine, actualizează timp la t+1 şi este
blocată prin wait() (dacă timp=t);
- fie reia while şi este blocată prin wait() (dacă timp≠t).
Deci toate persoanele sunt trezite (într-o ordine aleatoare), vor căpăta controlul
asupra monitorului şi vor executa activităţile de mai sus, după care sunt din nou blocate.
Să observăm că poate fi reluat şi firul m, dar executarea sa este amânată prin
sleep până la scurgerea unei unităţi de timp.
Suntem deci înaintea momentului de timp t+1 şi am revenit la situaţia dinaintea
momentului de timp t.

Observaţie. Unitatea de timp (aici 500 milisecunde) trebuie să fie suficient de


mare pentru a permite tuturor persoanelor să-şi execute "scurta" activitate în cadrul
acestei unităţi de timp.

S-ar putea să vă placă și