În limbajul Java, sincronizarea firelor de execuție se face prin intermediul monitoarelor. Se numește monitor instanța unei clase care conține cel puțin o metodă sincronizată, sau o metodă care conține un bloc sincronizat. Se numește metodă sincronizată orice metodă care conține în antetul său modificatorul synchronized, deci este declarată sub forma
[modif]synchronized tip nume_metoda(declaratii_argumente)
{corpul_metodei}
unde modif reprezinta alți eventuali modificatori (public, static etc.).
Când un fir începe executarea uni metode sincronizate a unui monitor, el devine "proprietarul" monitorului căruia îi aparține această metodă (engleză: owner) și deține această calitate până la încheierea executării metodei sincronizate respective, sau până când se autosuspendă invocând metoda wait(), așa cum vom explica ulterior. Pe toata durata cât un fir de execuție este proprietarul unui monitor, nici un alt fir nu poate invoca o metodă sincronizată a monitorului respectiv. Așa dar, orice fir care, în acest interval de timp, ar încerca să invoce o metodă sincronizată a unui monitor al cărui proprietar este alt fir, trebuie să aștepte până când monitorul respectiv este eliberat de proprietarul existent.
În acest fel, se evită situațiile în care un fir de execuție ar
interveni să facă modificări asupra unui obiect, în timp ce acesta se
găsește deja în curs de prelucrare de către un alt fir. De exemplu, dacă
un fir de execuție efectuează ordonarea datelor dintr-un tablou, nu
este corect ca un alt fir, în acest timp, să modifice datele din acest
tablou.
Metoda
public final void wait()
throws InterruptedException
și variantele ei
public final void wait(long timeout)
throws InterruptedException
public final void wait(long timeout, int nanos)
throws InterruptedException
au ca efect trecerea firului de execuție activ din
starea Running în starea Waiting (vezi schema). Metoda wait()
fără argumente face această trecere pentru un timp nedefinit, în timp ce
celelalte două metode primesc ca argument intervalul de timp maxim
pentru care se face suspendarea execuției firului.
Metodele
public final void notify()
public final void notifyAll()
au ca efect trecerea firului de execuție din starea Waiting în
starea Ready, astfel încât el poate fi activat de către dispecer
în momentul în care procesorul sistemului devine disponibil. Deosebirea
dintre ele este ca metoda notify() trece în starea Readyun
singur fir de execuție dintre cele care se gasesc în momentul
respectiv în starea Waiting provocată de acest monitor (care
din ele depinde de implementare), în timp ce notifyAll() le
trece în starea Ready pe toate.
Clasa monitor concepută pentru asigurarea relației producător/consumator are următoarea formă:
class NumeClasaMonitor {
//
declarații câmpuri de date ale monitorului
boolean variabilaConditie=false;
[public] synchronized void puneDate(declarații_argumente) {
if(variabila_condiție) {
try {
wait(); // se asteapta folosirea
datelor puse anterior
}
catch(InterruptedException e) {
/*
instrucțiuni de executat dacă a apărut o excepție de întrerupere */
}
}
/*
punerea de date noi în câmpurile de date ale monitorului */
variabilaConditie=true; // s-au pus date noi
notify(); // se notifica punerea de noi
date
} //
sfarsit puneDate
[public] synchronized tip
preiaDate(declaratii_argumente)
{
if(!variabilaConditie) {
try {
wait(); // se așteaptă să se pună
date noi
}
catch(InterruptedException e) {
/*
Instrucțiuni de executat dacă a apărut o excepție de întrerupere */
}
}
/*
instrucțiuni prin care se folosesc datele din monitor și se formează
valoarea_întoarsă */
variabilaConditie=false; // datele sunt deja folosite
notify(); // se notifică folosirea
datelor
return valoarea_intoarsa;
} //
sfarsit preiaDate
/*
Alte metode ale monitorului (sincronizate sau nesincronizate) */
} //
sfarsit clasa monitor
Inițial, variabila variabilaConditie a monitorului are valoarea false, întrucat - deocamdată - nu există date noi.
Dacă firul de execuție producător a ajuns în starea, în care trebuie să pună în monitor date noi, el invocă metoda sincronizată puneDate, devenind astfel proprietar (owner) al monitorului. În această metodă, se verifică, în primul rând, valoarea variabilei variabilaConditie. Dacă această variabilă are valoarea true, înseamnă că în monitor există deja date noi, înca nefolosite de consumator. În consecință, se invocă metoda wait() și firul de execuție monitor este suspendat, trecând în starea Waiting, astfel că el încetează să mai fie proprietar (owner) al monitorului. Dacă un alt fir (în cazul de față consumatorul) invocă o metodă sincronizată a aceluiași monitor care, la rândul ei, invocă metoda notify() sau notifyAll(), firul pus în așteptare anterior trece din starea Waiting în starea Ready și, deci, poate fi reactivat de către dispecer. Dacă variabila variabilaConditie are valoarea false, firul producător nu mai intră în așteptare, ci execută instrucțiunile prin care modifică datele monitorului, după care pune variabilaConditie la valoarea true și se invocă metoda notify() pentru a notifica consumatorul că exista date noi, după care se încheie executarea metodei puneDate.
Funcționarea firului de execuție consumator este asemănătoare, dar acesta invocă metoda preiaDate. Executând această metodă, se verifică mai întâi dacă variabilaConditie are valoarea false, ceeace înseamnă că nu există date noi. În această situație, firul consumator intră în așteptare, până va primi notificarea că s-au pus date noi. Dacă, însă, variabilaConditie are valoarea true, se folosesc datele monitorului, după care se pune variabilaConditie la valoarea false, pentru a permite producătorului să modifice datele și se invocă metoda notify() pentru a scoate producătorul din starea de așteptare.
Este foarte important să avem în vedere că
metoda puneDate este concepută pentru a fi invocată de firul
producător, în timp ce metoda preiaDate este concepută pentru a fi invocată de către firul
consumator. Numele folosite aici atât pentru clasa monitor, cât și
pentru metodele acesteia și variabila de condiție sunt convenționale și
se aleg de către programator, din care cauză au fost scrise în cele de
mai sus cu roșu cursiv.
Exemplul 1 În fișierul Sincro.java se dă un exemplu de aplicație în care se sincronizeaza două fire de execuție folosind modelul producător/consumator. Pentru a ușura citirea programului, clasele au fost denumite chiar Producator, Consumator și Monitor, dar se puteau alege, evident, orice alte nume. Firul de execuție prod, care este instanța clasei Producator, parcurge un număr de cicluri impus (nrCicluri), iar la fiecare parcurgere genereaza un tablou de numere aleatoare, pe care îl depune în monitor. Firul de execuție cons, care este instanță a clasei Consumator, parcurge același număr de cicluri, în care folosește tablourile generate de firul prod. Transmiterea datelor se face prin intermediul monitorului monit, care este instanță a clasei Monitor. Această clasă conține tabloul de numere întregi tab și variabila de condiție valoriNoi, care are inițial valoarea false, dar primește valoarea true atunci când producătorul a pus în monitor date noi și primește valoarea false, atunci când aceste date au fost folosite de consumator. Pentru punerea de date noi în monitor, producătorul invocă metoda sincronizată puneTablou. Pentru a folosi datele din monitor, consumatorul invocă metoda sincronizată preiaTablou. Fiecare din aceste metode testează valoarea variabilei de condiție valoriNoi și pune firul de execuție curent în așteptare, dacă valoarea acestei variabile nu este corespunzătoare. După ce s-a efectuat operația de punere/utilizare a datelor, metoda sincronizată prin care s-a făcut această operație invocă metoda notify(). Dacă ar fi putut exista mai multe fire în starea Waiting, era preferabil sa se invoce metoda notifyAll().
Referitor la acest program, remarcăm urmatoarele: Iată un exemplu de executare a acestei aplicații:
Se observă că, deși cele două fire se derulează în mod autonom, exista între ele o sincronizare corectă, în sensul că datele puse de producător la un ciclu cu un anumit indice, sunt preluate de consumator la ciclul cu același indice (de exemplu datele puse de producator în ciclul 3 sunt luate de consumator tot în ciclul 3). Așa dar, nu există date pierdute sau preluate de două ori. |
Exemplul 2 În fișierul Bondari1.java se dă un exemplu de aplicație cu interfață grafică, în care există trei fire de execuție (în afară de cel al metodei main()): două fire din clasa Bondar, care calculeaza fiecare mișcările unui "bondar", și un fir de executie din clasa Fereastră, care reprezintă grafic mișcările celor doi bondari. În acest caz, ambii "bondari" se mișca în aceeași fereastră. Pentru că fiecare din acestea extinde câte o clasa de interfață grafică (respectiv clasele JPanel și Canvas), pentru realizarea firelor de execuție s-a folosit interfața Runnable. Cele două fire "bondar" (respectiv fir1 și fir2) au rolul de producător, iar firul care conține fereastra de afișare (respectiv fir3) are rolul de consumator. Rolul monitorului este îndeplinit de instanța clasei CutiePoștală, care nu conține decât variabila de condiție a monitorului valoareNoua și două metode sincronizate amPus și amLuat. Având în vedere că există posibilitatea ca, la un moment dat, să existe în starea Waiting mai multe fire de așteptare care trebuie reactivate, în aceste metode s-a folosit invocarea notifyAll() în loc de notify(). Dacă se execută această aplicație se poate vedea cum cei doi "bondari" evoluează în mod independent în aceeași fereastră, putându-se ajusta în mod independent perioada și amplitudinea fiecăruia. |