Sunteți pe pagina 1din 68

Java concurrency in practice (rezumat - Romana)

Capitolul 2 Thread Safety(Scrierea firelor in conditii de siguranta)


Cumva surprinzator, programarea concurenta nu este atat de mult
despre fire(threads) si lacate(locks), mai mult decat este ingineria civila
despre nituri si grinzi. Bineinteles, constructia de poduri rezistente necesita
folosirea corecta a o multime de nituri si grinzi, la fel cum programarea
concurenta necesita folosirea corecta a firelor de executie si a lacatelor. Dar
acestea sunt doar mecanisme menite pentru atingerea unui scop. Scrierea
codului firelor in conditii de siguranta, este la baza, despre gestionarea
accesului la stare(state), in particular despre starea partajata,mutabila.
In mod informal, starea unui obiect este reprezentata de datele
acestuia, stocate in variabile de stare precum cele de instanta sau
statice.Starea unui obiect poate cuprinde campuri(fields) din alte obiecte de
care depinde; De exemplu starea unui HashMap este stocata in obiectul
HashMap in sine, dar de asemenea, dar de asemenea in multe obiecte
Map.Entry .Starea unui obiect contine orice date care afecteaza
comportamentul sau vizibil din exterior.
Prin partajat(shared), intelegem ca o variabila poate fi accesata de mai
multe fire de executie (threads); Prin mutabila(mutable), intelegem ca starea
variabilei se poate schimba pe durata ei de viata. Putem sa vorbim despre
scrierea firelor in conditii de siguranta (thread safety) precum si despre
codarea lor, dar ceea ce incercam cu adevarat sa facem este sa protejam
datele de acces concurrent necontrolat.
Un obiect trebuie sa fie thread-safe daca v-a fi accesat de fire de executie
multiple. Aceasta este o proprietate a modului in care un obiect este folosit
intr-un program,si nu ceea ce face. Pentru a face un obiect thread safe
avem nevoie de folosirea sincronizarii pentru accesul la starea sa mutabila
(schimbatoare) iar nefolosirea sincronizarii are ca rezultat alterarea
datelor(data corruption) si alte consecinte nedorite.
Oricand mai mult de un fir de executie acceseaza o variabila de stare,
si unul dintre fire poate scrie in aceasta, toate firele trebuie sa-si
coordoneze accesul folosind sincronizarea. In java, principalul mecanism de
sincronizare este cuvantul cheie synchronized, care ofera blocare
exclusiva(exclusive locking), dar cuvantul cheie sicronizare, de asemenea
include : folosirea de variabilelor volatile, blocari explicite, si variabile
atomice(atomic variables).
Trebuie sa evitam tentatia de a crede ca exista situatii speciale in care
aceste reguli nu se aplica. Un program care omite nevoia de sincronizare
poate functiona aparent, poate trece testele si functiona bine mai multi ani,
dar este inca incorect si poate da gres in orice moment.
Daca mai multe thread-uri acceseaza acceasi variabila cu stare mutabila
fara sicronizarea necesara, programul este incorect. Exista 3 posibilitati sa
rezolvam aceasta problema :
1) nu partajam variabila de stare intre fire de executie multiple
2) facem variabila de stare nemutabila (immutable)
3) folosim sincronizarea oricand dorim sa accesam variabila de stare
Daca nu ati luat in considerare accesul concurent in designul claselor ,
aceste principii necesita modificari semnificative de desing, deci rezolvarea
problemei poate sa nu fie asa de triviala precum aceste sfaturi suna. Este
mult mai usor sa gandim o clasa thread-safe de la inceput, decat sa o
modificam pentru a fi thread-safe.
Intr-un program mare, identificarea multiplelor thread-uri care pot
accesa o variabila poate fi complicata. Din fericire, aceleasi tehnici OO care
te ajuta sa scrii clase bine organizate, usor de intretinut, precum incapsulare
si ascunderea datelor, te pot ajuta sa creezi clase sigure din punct de
vedere al accesului firelor. Cu cat mai putin cod are acces la o variabila
particulara, cu atat mai usor este sa ne asiguram ca toate folosesc
sincronizariea potrivita, si cu atat este mai usor sa gandim despre conditiile
in care o variabila data poate fi accesata. Limbajul Java nu te forteaza sa
incapsulezi starea – este perfect permis sa o stochezi in variabile publice
(chiar statice) sau sa publici o referinta la alt obiect intern – dar cu cat este
mai bine incapsulata starea unui program, cu atat este mai usor sa faci
programele thread-safe si sa-i ajuti pe cei care le intretin.
Cand proiectam clase thread-safe tehnicile de proiectare orientata
obiect (OO) sunt cel mai buni prieteni (incapsularea, neimutibilitatea si
specificarea clara a invariantilor).
Vor fi momente cand tehnicile OO sunt in conflict cu cerintele reale;
Poate fi necesar in aceste cazuri sa facem un compromis in regulile de
design corect pentru performanta sau pentru compatibilitate. Cateodata,
abstractizarea si incapsularea sunt in conflict cu performanta – desi multi
dezvoltatori nu cred acest lucru, este de preferat sa faci codul in primul rand
corect si abia apoi sa ruleze rapid.
Daca decizi sa trebuie sa renunti la incapsulare, nu este totul pierdut. Este
inca posibil sa faci programul thread-safe, desi mult mai dificil. In plus
siguranta firelor de executie v-a fi mult mai fragila, implicand nu numai
costuri suplimentare de development, dar si costuri de intrinere de
asemenea.
Am folosit termenii “thread-safe class” si “thread-safe program” aproape
reciproc pana acum. Este un program thread-safe unul construit numai din
clase thread-safe? Nu neaparat – un program construit numai din clase
thread-safe poate sa nu fie thread-safe, dar si invers.
De problemele care inconjoara compozitia claselor thread-safe se ocupa
capitolul 4. In orice caz, conceptul de clasa thread-safe are sens doar daca
clasa incapsuleaza propria stare. Thread-safe poate fi un termen ca se
aplica la cod, desi este despre stare,si poate fi aplicat la intreg corpul
codului care incapsuleaza starea, care poate fi un obiect sau un intreg
program.

2.1 Ce reprezinta siguranta firelor (thread safety)


Definirea unui thread poate fi surprinzator de complicat. Incercarile mai
formale sunt atat de complicate incat ofera putina indrumare practica sau
intelegere intuitiva, si restul sunt descrieri informale care pot parea circulare.
La o simpla cautare Google ne intoarce multiple definitii precum acestea :
“. . . can be called from multiple program threads without unwanted
interactions between the threads.”
“. . . may be called by more than one thread at a time without requiring any
other action on the caller’s part.”

Date fiind aceste definitii, nu este de mirare ca gasim confuza


siguranta firelor . Nu poti sa nu sustii intradevar o astfel de propozitie, dar
nici nu ofera foarte mult ajutor practic.
Cum deosebim o clasa thread-safe de una unsafe. Ce intelegem prin
“safe “?
La baza oricarei definitii acceptate de siguranta a firelor de executie
(thread safety) este conceptul de corectitudine. Daca definitiile noastre sunt
neclare, aceasta este deorece nu avem o definitie clara a corectitudinii.
Corectitudinea (correctness) inseamna ca o clasa este conform
specificatiilor. O buna specificare defineste invarianti(invariants) care
constrang starea unui obiect si postconditii(postconditions) care descriu
efectele propriilor operatii. Atata timp cat nu scriem specificatii adecvate
pentru clasele noastre, cum putem sa spunem ca sunt corecte ?
Nu putem, dar aceasta nu ne poate opri sa sa le folosim oricum atat
timp cat suntem convinsi ca acel cod merge. Aceasta incredere in cod este
la fel de aproape cum multi dintre noi percep corectitudinea, deci puteam sa
presupunem corectitudinea programelor cu un singur fir de executie ca ceva
care “il stim cand in vedem”.Dupa definirea optimista a corectitudinii ca ceva
care poate fi recunoscut, putem defini siguranta firelor intr-un mod circular :
o clasa este thread-safe daca se comporta corect cand este accesata din
thread-uri multiple.

O clasa este thread safe daca se comporta corect cand este accesata din
multiple fire, indifferent de programare sau de intercalarea executarii
acestor fire, si fara sincronizare aditionala sau coordonare a unei parti din
codul apelat.
Cum fiecare program cu un singur fir de executie este de asemenea un
program valid cu mai multe fire de executie, nu poate fi thread-safe daca nu
este corect intr-un mediu cu un singur fir de executie. Daca un obiect este
corect implementat, nici o secventa de operatii – apeluri catre metode si
scrieri sau citiri de campuri publice – nu poate fi capabila sa incalce vreo
invarianta sau post-conditie. Nici un set de operatii effectuate secvential sau
concurent pe instante ale unei clase thread-safe nu poate cauza ca o
instanta sa fie in stare invalida.
Clasele thread-safe incapsuleaza orice nevoie de sincronizare, astfel incat
clientii nu trebuie sa isi faca griji in privinta acesteia(sincronizarii).
2.1.1 Exemplu : un servlet
In capitolul 1, am enumerat framework-uri care creeaza thread-uri si
apeleaza componente din aceste thread-uri, lasandu-ne responsabilitatea
de a face componentele thread-safe. Foarte des, cerintele thread-safety nu
provin dintr-o decizie de a folosi thread-uri, ci dintr-o decizie de a folosi spre
exemplu framework-ul Servlets. Vom scrie un exemplu simplu – un servlet
pentru system de factorizare.
@ThreadSafe
public class StatelessFactorizer implements Servlet {

public void service(ServletRequest req, ServletResponse resp) {


BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
encodeIntoResponse(resp, factors);
}

Un thread care acceseaza StatelessFactorizer nu poate influenta rezultatul


unui alt thread care acceseaza acelasi StatelessFactorizer.; pentru ca doua
thread-uri nu partajeaza starea(do not share state),este ca si cum ar fi
accesate de instante diferite. Astfel, actiuni ale firelor de executie asupra
unui astfel de obiect (stateless) nu pot afecta corectitudinea operatiilor in
alte thread-uri, deci obiectul este thread-safe.
Faptul ca majoritatea servlet-urilor pot fi implementate fara o stare anume
(no state) reduce sarcina de a face servlet-urile thread-safe. Doar cand
servlet-urile au nevoie sa tina minte lucruri de la un request la altul pot
devein o problema din punct de vedere al sigurantei firelor de executie
(thread-safety).
2.2 Atomicitate
Ce se intampla cand adaugam un element de stare la ceea ce era
inainte un obiect fara stare(stateless). Sa presupunem ca adaugam un
contor de apeluri, care masoara numarul de request-uri procesate. Cea mai
evidenta implementare adauga un camp de tip long servlet-ului si il
incrementeaza la fiecare request, asa cum se arata in codul de mai jos
@NotThreadSafe
public class UnsafeCountingFactorizer implements Servlet {
private long count = 0;
public long getCount() { return count; }
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
++count;
encodeIntoResponse(resp, factors);
}
}

Din pacate UnsafeCountingFactorizer nu este thread-safe, desi ar rula ok


intr-un mediu single-threaded. La fel ca UnsafeSequence din capitolul 1,
este susceptibil la pierderi de update. Operatia de incrementare,++count
desi poate parea ca o singura operatie datorita sintaxei compacte, ea nu
este atomica,ceea ce inseamna ca nu se executa ca o singura operatie,
indivizibila. In schimb, este o prescurtare pentru o serie de 3 operatii
discrete: preia valoarea curenta, adauga 1 unitate la ea, scrie inapoi
valoarea incrementata. Acesta este un exemplu de operatie citeste-
modifica-scrie(read-modify-write), in care starea rezultata este derivate din
starea precedenta.
Figura 1.1 din capitolul 1 ne arata ce se intampla cand doua thread-uri
incearca sa incrementeze un contor in mod simultan fara sincronizare.
In cazul in care contorul este 9 initial, cu putina sincronizare
ghinionista(unlucky timing), fiecare thread v-a citi valoarea, o v-a vedea ca
este 9, adauga 1 la ea,si seteaza fiecare counter la 10. Acest lucru nu este
dorit sa se intample. O incrementare este pierduta, iar acum
contorul(counter) este in urma cu o unitate.
Va puteti gandi ca a avea un numar usor inexact de apeluri intr-un serviciu
web-based este o eroare acceptata, si uneori chiar este. Dar in cazul in care
contorul este folosit pentru a genera secvente sau identificatori unici de
obiect, returnarea aceleiasi valori din invocari multiple poate cauza
probleme de integritate a datelor(serious data integrity problems).
Posibilitatea de a avea rezultate incorecte in prezenta sicronizarii
ghinioniste(unlucky timing) este atat de importanta in programarea
concurenta, incat are un nume : a race condition (conditii de cursa).
2.2.1 Conditii de cursa(race conditions)
UnsafeCountingFactorizer are multiple race conditions care fac
rezultatele nestabile(nedemne de de incredere).O conditie de cursa(race
condition) se intampla cand corectitudinea calculului depinde de
sincronizarea relativa sau intercalarea a multiple thread-uri; in alte cuvinte
cand primirea unui rezultat bun depinde de sincronizarea norocoasa(lucky
timing). Cel mai comun tip de race-conditions este check-then-act, unde un
potentiala observatare invechita(stale observation),este folosita pentru a lua
o decizie cu ce se v-a face in viitor.
Foarte des intalnim un race-condition in viata reala. Sa presupunem
ca planuiai sa te intalnesti cu un prieten la pranz in Starbucks la University
Avenue. Dar cand ajungi acolo, realizezi ca sunt doua Starbucks, si nu esti
sigur la care v-ati dat intalnire. La 12:10, iti cauti prietenul la Starbucks A,
dar acesta nu este acolo, asa ca mergi la Starbuck B dar nici acolo nu il
gasesti. Sunt cateva posibilitati: prietenul tau a intarziat si nu este la niciuna
din Starbucks, prietenul tau a ajuns la Starbucks A dup ce ai plecat, sau
prietenul tau a fost la Starbucks B,nu te-a gasit, si acum este in drum spre
Starbuck A. Sa presupunem ca se intampla ce e mai rau si sa spunem ca
ultima varianta s-a intamplat. Acum, la 12:15 ai fost la ambele StarBucks, si
amandoi va intrebati daca ati ajuns. Ce faci acum ? Te intorci la celalalt
StarBucks ? De cate ori v-a fi nevoie sa te intorci ? Daca nu aveti un
protocol cunoscut de amandoi, puteti petrece intreaga zi plimbandu-va intre
cele 2 StarBucks-uri.
Exemplul StarBucks ilustreaza conditii de cursa, deoarce atingerea
scopului dorit (intalnirea cu prietenul), depinde de diferenta de timp
relativa(relative timing) a evenimentelor(cand unul din voi ajunge la
StarBucks, cat timp asteapta pana cand v-a merge la celalat
StarBucks?).Observatia ca nu este la StarBucks A devine potential
invalidada de indata ce iesi pe usa din fata; poate a intrat pe usa din spate
si tu nu stii acest lucru. Ceea ce caracterizeaza cele mai multe race-
conditions este nevalidarea observarii – folosirea unei observatii invalide
pentru a face un viitor calcul. Acest tip de race-condition este numit check-
then-act(verifica si apoi actioneaza). Observi ceva ce pare adevarat (de ex
fisierul X nu exista, si apoi iei o actiune bazata pe acea observatie (creeaza
X), dar de fapt observatia poate deveni invalida intre timpul in care ai
observant si timpul in care actionezi (cineva a creeat fisierul X intre timp),
cauzand o problema(unexpected exception,overwritten data, file corruption)
– exceptie neasteptata, date scrise peste alte date, fisier corup.

2.2.2 Exemplu : conditii de cursa in initializarea lenesa


(race conditions in lazy initialization)
O expresie idiomatica care foloseste check-then-act este initalizarea
lenesa(lazy initialization). Scopul initializarii lenese este sa intarzii inializarea
unui obiect pana cand este nevoie de acesta, in acelasi timp asigurandu-ne
ca este intializat doar o data.
LazyInitRace din figura urmatoare ilustreaza expresia lazy-inialization.
Metoda getInstance mai intai verifica daca un obiect ExpensiveObject a fost
deja intializat, in acest caz returnand obiectul; altfel creeaza o noua instanta
si o returneaza retinand instant pentru a evita cod mai “scump”(expensive
code path).
@NotThreadSafe
public class LazyInitRace {
private ExpensiveObject instance = null;
public ExpensiveObject getInstance() {
if (instance == null)
instance = new ExpensiveObject();
return instance;
}
}

LazyInitRace are conditii de cursa(race conditions) care pot submina


corectitudinea ei. Sa presupunem ca thread-urile A si B executa getInstance
in acelasi timp. A vede ca obiectul este nul si instantiaza un nou obiect
ExpensiveObject. B de asemenea verifica daca instanta este nula. Acest
lucru depinde de sincronizare(timing), inclusiv de evenimentele imprevizibile
de programare,si de cat timp ii ia lui A sainstantieze expensiveObject si sa
seteze campul instanta. Daca instance e null cand B il observa, cele 2
apeluri vor avea rezultate diferite, chiar daca getInstance se presupune ca
trebuie sa intoarca mereu aceeasi instanta.
Operatia de incrementare contor (hit-counting operation) in
UnsafeCountingFactorizer are un alt fel de race condition(conditie de
cursa). Operatiile read-modify-write, precum incrementarea unui contor,
defines o transformare a starii unui obiect bazate pe starea precedent a sa.
Pentru a incrementa un contor trebuie stii valoarea sa initiala si sa te asiguri
ca nimeni nu foloseste acea valoare in mijlocul updatarii/modificarii.
Precum majoritatea erorilor de programare concurenta, conditiile de
cursa(race conditions) nu esueaza neaparat: o sincronizare
norocoasa(lucky timing) este de asemenea necesara. Dar race conditions
pot cauza probleme serioase. Daca LazyInitRace este folosit pentru a
instantia registrii unei aplicatii, trebuind sa intoarce diferite instante din
multiple invocari poate face ca registrii sa fie pierduti sau multiple activitati
sa aiba vizualizari incompatibile(inconsistent views) ale setului de obiecte
inregistrate. Daca UnsafeSequnce este folosit pentru a genera identificatori
unici intr-un framework persistence, doua obiecte distincte pot sa aiba in
final acelasi ID, nerespectand constrangerile de integritate a datelor.

2.2.3 Actiuni compuse(compound actions)


Atat LazyInitRace cat si UnsafeCountingFactorizer contin o secventa de
operatii care trebuie sa fie atomica sau indivizibila, relativ la alte operatii pe
aceeasi stare. Pentru a evita race conditions, trebuie sa fie o metoda de a
preveni alte thread-uri de la folosirea unei variabile cat timp suntem in
mijlocul modificarii ei, si astfel sa ne asiguram ca alte thread-uri pot sa
observe sau modifice starea doar inainte sa incepem sau dupa ce
terminam, dar nu in mijlocul modificarii.
Operatiile A si B sunt atomice in raport cu fiecare, daca, din persepctiva
unui thread care executa A, cand un alt thread executa B, ori B a executat
tot ce avea, ori nu a executat nimic. O operatie atomica este una care este
atomica in raport cu toate celelalte operatii, inclusiv ea insasi, care
opereaza pe aceeasi variabila de stare.

Daca operatia de incrementare in UnsafeSequence ar fi fost atomica,


conditiile de cursa din figura 1.1 nu s-ar fi produs, si fiecare operatie ar fi
avut ca rezultat incerementarea cu 1 a contorului. Pentru a asigura thread-
safety(siguranta firelor de executie), operatiile check-then-act si read-
modify-write(precum incrementarea) trebuie sa fie atomice.
Ne referim impreuna la operatiile check-then-act si read-modify-write ca
operatii compuse (compound operations): operatii care trebuie executate
atomic/impreuna pentru a fi thread-safe.
In urmatoarea sectiune, vom lua in considerare blocarea(locking),
mecanismul construit in Java pentru a asigura atomicitatea. Dar acum, vom
rezolva problema folosind o clasa thread-safe existenta, cum veti vedea in
exemplul urmator :

@ThreadSafe
public class CountingFactorizer implements Servlet {
private final AtomicLong count = new AtomicLong(0);
public long getCount() { return count.get(); }
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
count.incrementAndGet();
encodeIntoResponse(resp, factors);
}
}

Pachetul java.util.concurrent.atomic contine clase pentru variabile atomice,


pentru a efectua operatii atomice de stare pe numere si obiecte referinta.
Inlocuind contorul long cu AtomicLong, ne asiguram ca toate actiunile care
acceseaza contorul sunt atomice.
Am fi fost de asemenea capabili sa adaugam un contor la servlet-ul de
factorizare si sa mentinem thread-safety folosind p clase thread-safe pentru
a face operatii, si anume AtomicLong.
Cand un singur element de stare este adaugat la o clasa fara
stare(stateless class), clasa rezultata v-a fi thread-safe doar daca starea
este gestionata de o variabila thread-safe.Dar, asa cum vom vedea in
urmatoarea sectiune, trecerea de la o variabila de stare la mai multe nu este
neaparat asa de simpla ca trecerea de la zero la una.
Unde este practic, folosim obiecte thread-safe, precum AtomicLong, pentru
a gestiona starea unei clase. Este mai simplu a gandi despre posibilele stari
si tranzitii de stare pentru obiectele thread-safe existente decat este pentru
variabile cu stare arbitrara, si acest lucru face mai usor sa mentinem si sa
verificam thread-safety.
2.3 Blocarea/Lacatele/Incuierea (Locking)
Putem sa adaugam o variabila de stare servletului nostru si sa mentinem
thread-safety folosind un obiect thread-safe pentru a gestiona intreaga stare
a servlet-ului. Dar daca vrem sa adaugam si mai multa stare servlet-ului
putem sa adaugam mai multe variabile de stare thread-safe ?
Sa ne imaginam ca vrem sa crestem performanta servlet-ului , capturand
cel mai recent rezultat calculate, doar in cazul in care 2 request-uri de la
clienti consecutivi pe acelasi numar. (Aceasta este improbabil sa fie o
strategie de capturare; vom oferi una mai buna in sectiunea 5.6). Pentru a
implementa aceasta strategie, trebuie sa retinem 2 lucruri : ultimul numar
factorizat si factorii sai.
Am folosit AtomicLong pentru a genstiona variabila de stare contor intr-o
maniera thread-safe; am putea poate sa folosim “varul” sau
AtomicReference, pentru a gestiona ultimul numar si factorii sai ? O
incercare de acest fel este prezentata in codul care urmeaza, clasa
UnsageCachingFactorizer.
@NotThreadSafe
public class UnsafeCachingFactorizer implements Servlet {
private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger[]> lastFactors= new
AtomicReference<BigInteger[]>();
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
if (i.equals(lastNumber.get()))
encodeIntoResponse(resp, lastFactors.get());
else {
BigInteger[] factors = factor(i);
lastNumber.set(i);
lastFactors.set(factors);
encodeIntoResponse(resp, factors);
}
}
}

Din pacate, aceasta abordare nu functioneaza. Desi referintele atomice sunt


in mod individual thread-safe, UnsafeCatchingFactorizer are race conditions
care pot produce rezultate gresite.
Definitia “thread safety”(siguranta firelor de executie) cere ca invariantii sa
fie conservati, indiferent de sincronizare, sau de intercalarea de operatii
intre mai multe thread-uri (fire de executie).Un invariant (a function, quantity,
or property that remains unchanged when a specified transformation is
applied.) al UnsafeCatchingFactorizer este ca produsul factorilor din
lastFactors este egal cu valoarea lui lastNumber; servletul nostru este
corect doar atata timp cat invariantutul acesta este valabil. Cand multiple
variabile participa intr-un invariant, acestea nu sunt independente: valoarea
uneia cosntrange valoarea permisa a celorlalte. Astfel, cand facem update
asupra uneia, trebuie sa facem update asupra celorlalte in aceeasi operatie
atomica(same atomic operation).
Cu putina “sincronizare ghinionista”(unlucky timing),
UnsafeCatchingFactorizer poate incalca acest invariant. Folosind referinte
atomice, nu putem updata impreuna lastNumber si lastFactors simultan,
chiar daca fiecare apel la ele este atomic; Exista inca o fereastra de
vulnerabilitate, cand unul a fost modificat si celalat nu, si in acest timp este
posibil ca alte fire sa vada invariantul ca nevalabil.
Pentru a conserva consistenta starii, faceti update asupra variabilelor care
relationeaza intr-o singura operatie atomica.
2.3.1 Blocarea intrinseca (Intrinsic locks)
Java ne ofera un mecanism inclus pentru a forta atomicitatea : blocul
synchronized. (Este de asemenea un alt aspect critic al blocarii si alt
mecanism de sincronizare – vizibilitatea (visibility) care este acoperit in
capitolul 3). Un bloc synchronized are 2 parti: o referinta la obiectul care v-a
servi ca blocare/lacat si un bloc de cod care v-a fi pazit de acel lacat.
O metoda sincronizata este un mod mai scurt pentru un bloc synchronized
care cuprinde corpul unei intregi metode, si al carui blocare/lacat este
obiectul pe care metoda a fost invocata. (OBS : metodele statice
synchronized folosesc obiectul Clasa pentru blocarea lor)
synchronized (lock) {
// Access or modify shared state guarded by lock
}

Orice obiect Java poate implicit sa se comporte ca un lacat pentru scopuri


de sincronizare. Aceste lacate incorporate se numesc blocari intriseci sau
blocari monitor (intrinsic locks or monitor locks). Lacatul este obtinut
automat prin executarea unui fir de executie inainte de intrarea intr-un bloc
synchronized si eliberat cand controlul iese din blocul synchronized, fie prin
calea normala de control fie aruncand o exceptie(throwing an exception).
Singura cale de a obtine blocarea/lacatul intrinsec (intrinsic lock) este
intrarea intr-un bloc sau metoda synchronized pazit(a) de acea blocare.
Lacatele intrinseci(intrinsic locks) in java se comporta ca mutex-uri (sau
lacate de excludere mutuala(mutual exclusion locks)), ceea ce inseamna ca
cel mult un thread poate detine blocarea/lacatul. Cand thread-ul A incearca
sa obtina blocarea detinuta de thread-ul B, A trebuie sa astepte sau sa se
blocheze pana cand B elibereaza blocarea. Daca B nu elibereaza niciodata
lacatul, A asteapta pentru totdeauna.
Atat timp cat doar un thread o data poate executa un bloc pazit de un lacat
dat, blocurile synchronized pazite de acelasi lacat se executa atomic in
raport una fata de cealalta. In contextul programarii concurente,
atomicitatea inseamna acelasi lucru care inseamna si in aplicatiile
tranzationale – ca un grup de declaratii trebuie sa se execute ca o unitate
singura, indivizibila. Nici un thread care executa un bloc sincronizat nu
poate observa un alt thread sa fie in mijlocul unui bloc sincronizat, pazit de
acelasi lacat.
Mecanismul sincronizarii face usor sa restabilim thread safety la servletul
factory. Urmatorul cod face metoda service sincronizata, astfel incat un
thread poate intra in metoda service o data. SychronizedFactorizer este
acum thread-safe; totusi aceasta abordare este destul de extrema, atat timp
cat inhiba multipla ultilizare a mai multor clienti a servletului, rezultand in
responsivitate slaba, inacceptabila. Aceasta problema, care este o
problema de performanta, si nu de thread-safety (siguranta executarii
firelor) este rezolvata in sectiunea 2.5.

@ThreadSafe
public class SynchronizedFactorizer implements Servlet {
@GuardedBy("this") private BigInteger lastNumber;
@GuardedBy("this") private BigInteger[] lastFactors;
public synchronized void service(ServletRequest req,ServletResponse resp) {
BigInteger i = extractFromRequest(req);
if (i.equals(lastNumber))
encodeIntoResponse(resp, lastFactors);
else {
BigInteger[] factors = factor(i);
lastNumber = i;
lastFactors = factors;
encodeIntoResponse(resp, factors);
}
}
}

2.3.2 Reentrancy
Cand un thread cere lacatul care este detinut deja de alt thread, primul
thread se blocheaza. Dar deoarece lacatele intrinseci (intrinsic locks) sunt
reintrante (reentrant), daca un thread incearca sa dobandeasca lacatul pe
care il detine, acesta reuseste. Reentrancy inseamna ca blocarile/lacatele
sunt dobandite mai degraba pe un fir decat pe invocare.( Reentrancy means
that locks are acquired on a per-thread rather than per-invocation basis.)

Reentrancy este impementata prin asocierea cu fiecare lacat a unui contor


de achizitie si a unui thread pe care il detine. Cand contorul este 0, lacatul
este considerat liber. Cand un thread preia un lacat anterior liber, JVM
inregistreaza posesorul si seteaza contorul de achizitie la 1. Daca acelasi
thread incearca sa dobandeasca lacatul din nou, contorul este incrementat,
sic and threadul detinator al lacatului iese din blocul synchronized,
contorul(count) este decrementat. Cand contorul ajunge la 0, lacatul este
eliberat.
Reentrancy usureaza encapsularea comportamentului blocant, si simplifica
development-ul de cod OO.Fara lacate reentrante(reentrant locks), codul
care arata foarte natural in 2.7, in care o subclasa suprascrie o metoda
sincronizata si apoi apeleaza metoda din superclasa, ar ajunge la deadlock
(punct mort).
Deoarece metodele doSomething din Widget si LoggingWidget sunt
sincronizate, fiecare incearca sa obtina lacatul pe obiectul Widget, inainte
de a merge mai departe. Dar daca lacatele intrinseci nu ar fi reentrante
apelul la super.doSomething() nu ar fi cababil sa obtina lacatul, bentru ca s-
ar considera deja blocat, si thread-ul ar astepta permanent pentru un lacat
pe care nu il poate dobandi.Reentrancy ne salveaza de situatii ca cea de
mai jos :
public class Widget {
public synchronized void doSomething() {
...
}
}
public class LoggingWidget extends Widget {
public synchronized void doSomething() {
System.out.println(toString() + ": calling doSomething");
super.doSomething();
}
}

2.4 Pazirea starii cu lacate/blocari (Guarding state with


locks)
Deoarece lacatele permit accesul serializat la calea de cod pe care o
pazesc, le putem folosi pentru a construi protocoale care garanteaza acces
exclusiv la starea partajata(shared state).
Actiuni compuse pe starea partajata, cum sunt incrementarea unui
contor(read-modify-write) sau inializarea lenesa(check-then-act), trebuie sa
fie facute atomice pentru a evita conditiile de cursa(race-conditions).
Detinerea lacatului pe intreaga operatie a unei actiuni compuse, poate face
acea actiune compusa atomica. Totusi, inconjurarea unei intregi actiuni
compuse cu un bloc synchronized nu este suficienta;daca sincronizarea
este folosita pentru a coordona accesul la o variabila, ea este necesara
oriunde este accesata acea variabila.
Pentru fieare variabila de stare mutabila (care se poate schimba) care
trebuie sa fie accesata de mai mult de un thread, toate accesurile catre
acea variabila trebuie sa se desfasoare cu acelasi lacat tinut. In acest caz
spunem ca variabila este pazita de acel lacat.
Este o greseala comuna sa credem ca sincronizarea este folosita doar
unde aven de scris in o variabila partajata(shared); acest lucru nu este
adevarat. (Motivul va deveni mai clar in sectiunea 3.1)
In SynchronizedFactorizer din 2.6, lastNumber si lastFactors sunt pazite de
lacatul intrinsic al servletului (servlet object’s intrinsic lock).
Nu exista nici o relatie inerenta intre lacatul intrinsec al unui obiect si starea
sa; campurile unui obiect trebuie sa nu fie pazite de lacatul sau intrinsec,
desi aceasta este o conditie de blocare folosita de multe clase.Dobandirea
blocarii/lacatului(lock) asociat cu un obiect nu previne alte thread-uri sa
acceseze acel obiect – singurul lucru pe care dobandirea blocarii il previne
este dobandirea aceleiasi blocari/lacat. Faptul ca fiecare obiect are un lacat
incorporat in el, este doar un avantaj astfel incat sa nu fie necesar sa creezi
in mod explicit obiecte lacat . Este la indemana ta sa construiesti protocoale
de blocare sau politici de sincronizare care te ajuta sa accesezi variabile de
stare partajate in siguranta, si sa le folosesti intr-un mod consistent in
programe.
Fiecare variabila partajata, mutabila ar trebui pazita de exact un lacat.
Faceti clar la cei care mentin codul care lacat este.
O conventie comuna de blocare este sa incapsulezi toate starile mutabile
intr-un obiect si sa-l protejezi de acces concurrent sincronizand orice cod
care acceseaza starea mutabila folosind blocarea/lacatul intrinsec al
obiectului. Acest pattern este folosit de multe clase thread-safe, precum
Vector, dar si alte colectii de clase sincronizate. In astfel de cazuri, toate
variabilele de stare ale unui obiect sunt “pazite”(guarded) de propriul lacat
intrinsec. Totusi, nu este nimic special despre acest pattern, nici
compilatorul nici runtime-ul nu forteaza aceasta(sau oricare alt pattern) de
blocare(locking). Este usor sa distrugi acest protocol de blocare in mod
accidental, adaugand o noua metoda sau cod si uitand sa folosim
sincronizarea la ele.
Nu toate datele trebuie sa fie pazite de lacate, doar datele mutabile
care vor fi accesate din thread-uri multiple.
In capitolul 1, am demonstrat cum adaugarea unui simplu eveniment
asincron cum ar fi un TimerTask poate creea cerinte de thread-safety care
se resfrang asupra intregului program, mai ales daca variabilele de stare ale
programului sunt prost incapsulate. Sa consideram un program cu un singur
fir de executie care proceseaza o cantitate mare de date. Programele
single-threaded nu necesita sincronizare, pentru ca datele nu sunt partajate
intre thread-uri. Acum sa ne imaginam ca vrem sa adaugam un feature
pentru a creea snapshot-uri periodice ale progresului, astfel incat sa nu fim
nevoiti sa il restartam cand se blocheaza sau trebuie oprit. Ai putea sa alegi
sa faci acest lucru cu un TimerTask care se declanseaza la 10 minute,
salvand starea programului intr-un fisier.
Cum TimerTask va fi apelat dintr-un alt thread(unul gestionat de Timer),
orice date ale programului implicate in snapshoot vor fi acum accesate de 2
thread-uri : thread-ul programului principal (main thread) si thread-ul pentru
Timer. Acest lucru inseamna ca nu doar codul din TimerTask va folosi
sincronizare cand va accesa starea programului, dar si orice cod in restul
programului, care acceseaza aceleasi date. Ceea ce nu cerea sincronizarea
cere acum in cursul intregului program.
Definiții pentru invariant
a function, quantity, or property that remains unchanged when a specified transformation is applied.
"For example, in Euclidean geometry, the relevant invariants are embodied in quantities that are not
altered by geometric transformations such as rotations, dilations, and reflections."

Cand o variabila este pazita de un lacat – insemnand ca orice acces la acea


variabila se face cu lacatul blocat – ne asiguram ca doar un thread o data
poate accesa acea variabila. Cand o clasa are invarianti care implica mai
mult de o variabila de stare, apare o cerinta suplimentara : fiecare variabila
care participa in invariant trebuie pazita de acelasi lacat. Acest lucru ne
permite sa le accesam sau updatam intr-o singura operatie atomica,
conservand invariantul. SynchronizedFactorizer demonstreaza aceasta
regula: atat numarul receptionat(cached) cat si factorii receptionati(cached)
sunt paziti de lacatul intrinsec al obiectului servlet.
Pentru fiecare invariant care implica mai mult de o variabila, toate variabilele
implicate in invariant trebuie pazite de acelasi lacat.
Daca sincronizarea este leacul pentru race conditions, de ce nu am declara
fiecare metoda ca sincronizata ? Se pare ca o astfel de aplicare
nediscriminatorie a sincronizarii ar putea fi ori prea mult ori prea putin
despre sincronizare. Numai sincronizand fiecare metoda, cum face clasa
Vector, nu este suficient pentru a face operatiile compuse pe clasa Vector
atomice :
if (!vector.contains(element))
vector.add(element);

Aceasta incercare la o operatie put-if-absent (pune-daca-nu este) are race


conditions, desi operatiile contains si add sunt atomice. Asa cum metodele
sychronized pot face operatiile individuale atomice, lacate/blocari aditionale
sunt necesare cand multiple operatii sunt combinate in operatii compuse/
(Vezi capitolul 4.4 pentru niste a adauga in mod sigur operatii aditionale
atomice obiectelor thread-safe.) In acelasi timo, sincronizarea fiecarei
metode poate conduce la probleme de performanta sau de durata de
viata(liveness), asa cum am vazut in SynchronizedFactorizer .

2.5 Durata de viata si performanta(Liveness and


performance)
In UnsafeCatchingFactorizer, am introdus niste catching in servlet-ul de
factorizare in speranta ca performanta v-a creste. Caching-ul necesita o
stare partajata(shared state), care la randul ei necesita sincronizare pentru
a mentine integritatea acelei stari. Dar modul in care am folosit sincroniarea
in SynchronizedFactorizer o face sa se execute gresit. Politica de
sincronizare in SynchronizedFactorizer este sa pazim fiecare variabila de
stare cu lacatul intrinsec al obiectului servlet, si politica a fost implementata
prin sincronizarea intregii metode service. Acest abordare simpla a restabilit
siguranta, dar pentru un pret foarte mare.
Deoarece service este synchronized, doar un thread o data o poate
executa. Acest lucru submineaza(distruge) destinatia de folosire a
framework-ului servlet - acela ca servleturile pot executa mai multe
request-uri simultan - si poate rezulta in utilizatori dezamagiti, daca load-ul
este destul de mare. Daca servletul este in lucru, factorinzand un numar
mare, ceilalti clienti trebuie sa astepte pana cand requestul curent este
complet, inainte ca servletul sa inceapa sa factorizeze un nou numar. Daca
sistemul este multiprocesor, procesoarele vor ramane inactive, chiar daca
load-ul este mare. In orice caz, pana si request-urile efectuate pe durata
scurta, precum cele pentru care valoarea este cached, pot dura neasteptat
de mult, deoarece trebuie sa astepte dupa request-urile anterioare care
dureaza mult.
Figura de mai jos (2.1) ne arata ce se intapla cand request-uri multiple
sosesc pentru servletul de factorizare : intra intr-o coada si sunt gestionate
sevential.
Putem descrie aceasta aplicatie ca prezinta concurenta slaba (poor
concurency): numarul de invocari simultane este limitat nu numai de
disponibilitatea procesoarelor,dar si de structura/arhitectura aplicatiei in
sine. Din fericire, este usor sa imbunatatim concurenta servletului cat timp
putem mentine thread-safety, prin ingustarea domeniului de aplicare a
blocului synchronized. Trebuie sa fim atenti sa nu facem domeniul blocului
synchronized prea mic; nu am vrea sa divizam o operatie care ar trebui sa
fie atomica, in mai mult de un bloc synchronized.
Dar este rezonabil sa incercam sa excludem din blocurile synchronized
operatiile care dureaza mult, si care nu afecteaza starea partajata(shared
state), astfel incat alte thread-uri sa nu fie impiedicate sa acceseze starea
partajata in timpul unor operatii consumatoare de timp in progres.
CachedFactorizer in Figura de mai jos(2.8) restructureaza servlet-ul pentru
a folosi 2 blocuri synchronized, fiecare limitata la o sectiune scurta de cod.
Unul pazeste secventa check-then-act care testeaza daca putem returna
rezultatul cached si cealalta pazeste secventa updatarii numarului
cached(cached number) si factorii cached(cached factors).
Ca un bonus, am reintrodus contorul de apeluri, si adaugat un contor
cached de asemenea, updatandu-le in blocul initial synchronized. Deoarece
aceste contoare constituie variabile de stare mutabile si partajate, trebuie sa
folosim sincronizarea oriunde sunt accesate. Portiunea de cod care este in
afara blocurilor sincronizate opereaza exclusiv pe variabile locale, care nu
sunt partajate intre thread-uri si care nu necesita sincronizare

@ThreadSafe
public class CachedFactorizer implements Servlet {
@GuardedBy("this") private BigInteger lastNumber;
@GuardedBy("this") private BigInteger[] lastFactors;
@GuardedBy("this") private long hits;
@GuardedBy("this") private long cacheHits;
public synchronized long getHits() { return hits; }
public synchronized double getCacheHitRatio() {
return (double) cacheHits / (double) hits;
}
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = null;
synchronized (this) {
++hits;
if (i.equals(lastNumber)) {
++cacheHits;
factors = lastFactors.clone();
}
}
if (factors == null) {
factors = factor(i);
synchronized (this) {
lastNumber = i;
lastFactors = factors.clone();
}
}
encodeIntoResponse(resp, factors);
}
}

CachedFactorizer nu mai foloseste AtomicLong pentru contorul de


apeluri(hit counter), in schimb se revine la folosirea variabilei de tip long.
Ar fi sigur sa folosim aici AtomicLong, dar este mai putin beneficiu decat era
la clasa CountingFactorizer. Variabilele atomice sunt folositoare pentru a
efectua operatii atomice o singura variabila, dar cum deja folosim blocuri
synchronized pentru a construi operatii atomice, folsirea a 2 mecanisme de
sincronizare ar creea confuzie si nu ar oferi beneficii legate de performanta
sau de siguranta.
Regandirea CachedFactorizer ne ofera o balanta intre
simplitate(sincronizarea intregii metode) si concurenta(sincronizarea a cat
mai putine regiuni de cod). Primirea si eliberarea lacatului are ceva
probleme(some overhead), deci este de nedorit sa spargem blocuri
synchronized(precum factorizarea ++hits on propriul bloc synchronized),
chiar daca acest lucru nu ar compromite atomicitatea.
CachedFactorizer tine blocarea/lacatul cand acceseaza variabile de stare si
pe durata actiunilor compuse, dar il elibereaza inainte de a executa operatii
de lunga durata de factorizare. Acest lucru conserva thread safety fara a
afecta concurenta; liniile de cod din fiecare bloc synchronized sunt suficient
de scurte.
Decizia asupra cat de lungi sau de scurte trebuie sa fie blocurile
synchronized poate cere compromisuri intre fortele de proiectare
competitive, incluzand siguranta(care nu trebuie compromisa), simplicitatea
si performanta. Cateodata simplicitatea si performanta sunt una importriva
celeilalte, desi asa cum arata CachedFactorizer , o balanta rezonabila poate
fi gasita.
Exista o frecventa tensiune intre simplicitate si performanta. Cand
implementam o politica de sincronizare, trebuie sa rezostam tentatiei de a
sacrifica prematur simplicitatea( si sa compromitem potential siguranta) de
dragul performantei.
Mereu cand folosim blocarea/lacatul (locking), trebuie sa fim atenti la ce
face codul din bloc si cat de probabil este sa dureze un timp indelungat.
Tinerea unui lacat pe o perioada mare de timp, fie pentru ca executam o
operatie de calcul-intesiv fie ca executam o operatie cu blocare
potentiala( blocking operation) introduce riscul de liveness sau probleme de
performanta.
Evitati sa tineti lacatele in timpul unor operatii de durata sau a unor operatii
cu riscul de a nu se termina repede, cum sunt folsirea retelei sau a consolei
I/O(intrare-iesire).
3. Partajarea Obiectelor(sharing objects)
Am spus la inceputul capitolului 2 ca scrierea de programe concurente
corecte este in primul rand despre gestionarea accesului la starea partajata,
mutabila. Acel capitol a fost despre folosirea sincronizarii pentru a preveni
multiple thread-uri sa acceseze aceleasi date in acelasi timp; acest capitol
examineaza tehnicile pentru a partaja si publica obiecte, astfel incat sa fie
accesate in siguranta de thread-uri multiple.
Impreuna, consituie fundatia pentru a construi clase thread-safe si pentru a
structura in siguranta aplicatiile concurente folosind clasele din
java.util.concurrent .
Am vazut cum blocurile si metodele sincronizate asigura ca operatiile
se executa atomic,dar este o intelegere gresita ca sincronizarea este doar
despre atomicitate sau despre demarcarea zonelor critice de cod.
Sincronizarea are de asemenea un alt aspect semnificant, subtil :
vizibilitatea memoriei. Vrem nu numai sa prevenim un thread de a modifica
starea unui obiect cand alt thread il foloseste, dar si sa ne asiguram ca
atunci cand un thread face modificari, celelalte thread-uri le vad(schimbarile
care au fost efectuate). Dar fara sincronizare, acest lucru nu s-ar intampla.
Te poti asigura ca obiectele au fost publicate intr-un mod sigur, fie folosind
sincronizarea explicita, fie profitand de sincronizarea construita in clasele de
librarie.
3.1 Vizibilitate
Vizibilitatea este subtila pentru ca lucrurile care pot merge prost sunt
contraintuitive. Intr-un mediu single-threaded, daca scrii o valoare in o
variabila si o citesti mai tarziu fara sa intervina scrieri, te astepti sa primesti
aceeasi valoare inapoi. Acest lucru pare natural. Poate parea greu de
accepat la inceput, dar cand scrierile si citirile se realizeaza din thread-uri
multiple, nu se intampla acest lucru. In general, nu exista nici o garantie ca
un thread care citeste va vedea o valoare scrisa de un alt thread in timp util,
sau niciodata. Pentru a asigura vizibilitatea intre thread-uri trebuie sa
folosim sincronizare.
NoVisibility in figura urmatoare ilustreaza ce se poate merge prost
cand thread-urile partajeaza date fara sincronizare. 2 thread-uri main thread
si thread-ul reader acceseaza variabilele partajate(shared) ready si number.
Thread-ul main incepe thread-ul reader si apoi seteaza numer la 42 si ready
la true. Thread-ul reader se executa pana cand vede ready true, si apoi
afiseaza numarul. In timp ce poate parea evident ca NoVisibility va afisa 42,
este posibil de asemenea sa printeze 0, sau sa nu se termine nicioadata.
Deoarece nu foloseste sincronizare adecvata, nu e nici o garantie ca
valorile lui ready si number scrise de thread-ul main vor fi vizibile thread-ului
reader.
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
public void run() {
while (!ready)
Thread.yield();
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}

NoVisibility poate sa intre intr-o bucla(loop) pentru totdeauna deoarece


valoarea lui ready poate sa nu devina vizibila pentru thread-ul reader. Si mai
ciudat, NoVisibility poate sa printeze 0 deoarece scrierea catre ready poate
fi facuta vizibila thread-ului reader inainte de a scire numarul, un fenomen
cunoscut ca „reordering” . Nu este nici o garantie ca operatiile dintr-un
thread vor fi efectuate de alt thread in ordinea data de program, atat timp
cat reordonarea nu este detectabila in acel thread – chiar daca reordering
este aparent la thread-uri. Cand thread-ul main scrie intai in number si apoi
in ready fara sincronizare, thread-ul reader poate vedea aceste lucruri in
ordine inversa, sau chiar deloc.
In absenta sincronizarii, compiler-ul, procesorul si mediul de
executie(runtime) pot face lucruri cu adevarat ciudate cu ordinea in care
operatiile par sa se execute. Incercarile de a motiva ordinea in care actiunile
de memorie trebuie sa se intample iste in programele multithreaded
sincronizate care aproape sigur vor fi incorecte.
NoVisibilily este la fel de simplu, cum programele concurente pot fi- 2
thread-uri si 2 valori partajate – si totusi este inca prea usor sa ajungem la
concluzii gresite despre ce face sau chiar daca se va termina.
Argumentarea programelor insuficient sincronizate este prohibitiv de dificila.
Acest lucru poate parea putin infricosator si ar trebui sa fie. Din fericire,
exista o metoda simpla pentru a evita aceste probleme complexe :
totdeauna folositi sincronizarea adecvata cand datele sunt partajate intre
thread-uri.
3.1.1 Date invechite (Stale data)
NoVisibility demonstreaza una din caile pe care sincronizarea insuficienta a
programelor poate cauza rezultate surprinzatoare : date invechite. Cand
thread-ul reader citeste ready, poate vedea o valoare „expirata”. In afara de
cazul cand sincronizarea este folosita de fiecare data cand o variabila este
accesata, este posibil sa vedem o valoare invechita pentru aceea variabila.
Mai rau, invechirea(staleness) nu este totul sau nimic : un thread poate
vedea o valoare updatata a unei variabile si o valoare neupdatata a altei
variabile.
Cand hrana este veche, ea este inca comestibila, doar mai putin placuta.
Dar datele invechite pot fi mult mai periculoase. In timp ce un contor de
apeluri neupdatat intr-o aplicatie web poate sa nu fie asa rau, valorile
invechite pot cauza probleme de siguranta sau esecuri ale perioadei de
viata(liveness failures). In NoVisibility, valorile vechi pot cauza afisarea de
rezultate incorecte sau pot face programul sa nu se mai termine. Lucrurile
pot fi si mai complicate cu aceste valori vechi ale referintelor la obiecte, cum
sunt pointerii legaturi in implementarea listelor inlantuite (linked list).
Datele invechite pot cauza esecuri serioase si confuze, precum exceptii
neasteptate, structuri de date corupte, calcule inexacte sau intrarea in bucle
infinite.
MutableInteger din 3.2 nu este thread-safe deoarece campul valoare este
accesat atat din get cat si din set fara sincronizare. Printre alte hazarduri,
este susceptibil la valori invechite : daca un thread apeleaza set, alt thread
care apeleaza get poate sa vada sau sa nu vada noua valoare(update-ul).
Putem face MutableInteger thread-safe sincronizand getter-ul si setter-ul
asa cum se arata in 3.3 . Sincronizarea numai a lui setter-ul poate sa nu fie
suficienta, thread-urile care apleaza get pot sa vada valori invechite(stale
values).
@NotThreadSafe
public class MutableInteger {
private int value;
public int get() { return value; }
public void set(int value) { this.value = value; }
}
@ThreadSafe
public class SynchronizedInteger {
@GuardedBy("this") private int value;
public synchronized int get() { return value; }
public synchronized void set(int value) { this.value = value; }
}

3.1.2 Operatii pe 64 biti nonatomice


Cand un thread citeste o variabila fara sincronizare, poate vedea o valoare
invechita, dar cel putin poate vedea o valoare care a fost plasata de alt
thread fata de o valoare aleatorie. Aceasta garantie a sigurantei este numita
„out-of-thin-air safety”. Out-of-thin-safety se aplica la toate variabilele, mai
putin la cele pe 64 de biti, care nu sunt declarate volatile. (vezi 3.1.4)
Modelul de memorie Java cere ca operatiile fetch si store sa fie atomice, dar
pentru variabilele nevolatile long si double, JVM are dreptul de a le trata ca
2 operatii separate pe 32 de biti. Daca citirile si scrierile se produc in thread-
uri diferite , este posibil sa citesti un long nevolatile si sa primesti partea de
sus de 32 de biti a valorii si partea de jos de 32 biti a celeilalte. Astfel, chiar
daca nu iti pasa de valori invechite, nu este sigure sa folosim variabile long
mutabile si partajate in progame multithreaded decat daca sunt declarate
volatile sau sunt pazite de un lacat.
3.1.3 Lacatele si vizibilitatea
Blocarea/lacatul intrinsec(a) pot fi folosite pentru a ne asigura ca un thread
vede efectele altuia intr-o maniera predictibila, asa cum vedem in figura 3.1.
Cand Thread-ul A executa un bloc sychronized, si apoi thread-ul B intra in
blocul sychronized pazit de acelasi lacat, varabilele care erau vizibile lui A
pana sa elibereze blocarea/lacatul sunt garantat vizibile si lui B, cand acesta
preia lacatul.

Cu alte cuvinte, tot ceea ce A a facut in blocul sychornized este vizibil lui B
cand acesta executa blocul synchronized pazit de acelasi lacat.
Fara sincronizare nu exista o astfel de garantie !
Putem acum sa dam un alt motiv pentru regula care cere ca toate thread-
urile sa se sincronizeze pe acelasi lacat cand accesam o variabila mutabila
partajata – sa garantam ca valorile scrise de un thread se fac vizibile in alte
thread-uri. Altfel, daca un thread citeste o valoare fara sa tina lacatul
apropiat,alt thread poate vedea valori invechite.

Locking is not just about mutual exclusion; it is also about memory visibility.
To ensure that all threads see the most up-to-date values of shared
mutable variables, the reading and writing threads must synchronize on
a common lock.
3.1.4

Blocarea nu este doar despre excludere mutuala, este de asemenea despre


vizibilitatea memoriei. Pentru a ne asigura ca toate thread-urile vad valorile
updatate ale variabilelor mutabile partajate, threadurile care citesc si care
scriu trebuie sincronizate pe un lacat comun.
3.1.4 Variabile volatile
Limbajul Java ne ofera o alternativa, o forma mai slaba de sincronizare,
variabilele volatile, pentru a ne asigura ca update-urile la o variabila se
propaga predictibil in alte thread-uri. Cand un camp este declarat volatile,
compilerul si runtime-ul primesc o notificare ca aceste variabile sunt
partajate si ca operatiile pe ele nu ar trebui sa fie reordonate cu alte operatii
de memorie. Variabilele volatile nu sunt cached in registrii sau in caches
unde sunt ascunse de celelalte procesoare, asa ca o citire a unei variabile
volatile intotdeauna returneaza cea mai recenta valoare scrisa de un thread.
Un mod bun de a ne gandi la variabilele volatile este saa ne imaginam ca se
comporta precum clasa SynchronizedInteger din 3.3, inlocuind citirile si
scrierile ale variabilelor volatile cu get si set. Totusi, accesarea unei
variabile volatile nu actioneaza nici o blocare, facand variabilele volatile un
mod de sincronizare cu greutate-mai-mica decat synchronized.
Efectele vizibile ale variabilelor volatile se extind dincolo de valoarea
variabilei volatile insasi. Cand thread-ul A scrie intr-o variabila volatila si
ulterior thread-ul B citeste aceeasi variabila, valorile tuturor variabilelor care
erau vizibile lui A inainte de scrierea in variabile volatile devin vizibile lui B
dupa ce a citit variabila volatila. Deci, din punct de vedere al vizibilitatii
memoriei, scrierea intr-o variabila volatila este precum iesirea dintr-un block
synchronized si citirea unei variabile volatile este precum intrarea intr-un
bloc synchronized. Totusi, nu recomandam sa ne bazam foarte mult pe
variabile volatile pentru vizibilitate; codul care se bazeaza pe variabile
volatile pentru vizibilitate este mai greu de inteles decat codul care foloseste
lacate.
Folositi variabile volatile doar cand simplifica impelmentarea si verificarea
politicii de sincronizare; evitati folosirea variabilelor volatile vand verificarea
corectitudinii ar necesita motivarea subtila a vizibilitatii. Bunele folosiri ale
variabilelor volatile includ asigurarea vizibilitatii a propriei stari, aceea la
care obiectul face referire, sau idicarea ca un eveniment important din ciclul
de viata (precum initializare sau inchidere) s-a produs.

Figura 3.4 ilustreaza folosirea tipica a variabilelor volatile : verificarea unui


status flag (steag de stare) pentru a decide cand sa iesi din bucla (loop).
In acest exemplu, thread-ul nostru antropomorfizat incearca sa adoarma
pana cand metoda numararii de oi se termina. Pentru ca acest exemplu sa
functioneze, steagul asleep trebuie sa fie volatil. Altfel, threadul poate sa nu
observe cand asleep a fost setat de alt thread.
Puteam in schimb sa folosim blocarea pentru a asigura schimbarile la
asleep, dar acest lucru ar facut codul mai complicat.
volatile boolean asleep;
...
while (!asleep)
countSomeSheep();

Variabilele volatile sunt convenabile, dar au limitari. Cel mai comun mod de
intrebuintare al lor este pentru o completare, intrerupere sau steag de status
, precum este asleep in exemplul anterior. Variabilele volatile pot fi folosite
pentru alte tipuri de informatii de stare, dar mai multa grija este necesara
cand incercam acesta. De exemplu, semantica unei variabile volatile sunt
suficient de puternice pentru a face o operatie de incrementare atomica,
daca nu poti garanta ca variabila este scrisa doar dintr-un singur thread
(Variabilele atomice ofera suport atomic read-modify-write si pot fi adesea
folosite ca variabile volatile mai bune; vezi capitolul 15)
Blocarea/lacatele asigura atat vizibilitate cat si atomicitate; variabilele
volatile asigura numai vizibilitate.

Poti folosi variabile volatile doar cand urmatoarele criterii sunt indeplinite:
 Scrieri la o variabila nu depind de valoarea curenta; sau daca te
poti asigura ca un singur thread face update pe variabila;
 Variabila nu participa in invarianti cu alte variabile de stare
 Blocarea nu este necesara pentru nici un motiv cat timp variabila
este accesata

3.2 Publicarea si iesirea (Publication and escape)


Publicarea unui obiect inseamna sa-l facem accesibil codului din afara
domeniului sau de aplicare, cum ar fi prin stocarea unei referinte unde alt
cod o poate gasi, returnand-o dintr-o metoda non-privata, sau trecand-o la o
metodada intr-o alta clasa. In multe situatii, vrem sa ne asiguram ca
obiectele si componentele lor interne nu sunt publicate.
In alte situatii vrem sa publicam un obiect pentru folosire in mod general,
dar sa facem acest lucru intr-un mod thread-safe necesita sincronizare.
Publicarea variabilelor interne de stare poate compromite incapsularea,si
poate face mult mai greu sa conservam invarianti; publicarea obiectelor
inainte sa fie construite poate compromite siguranta firelor(thread safety).
Un obiect care este publicat cand nu ar trebui sa fie se numeste
scapat(escaped). Sectiunea 3.5 acopera expresiile pentru publicare in
siguranta, dar acum, vom vedea cum un obiect poate scapa/iesi(escape).
Cel mai tipatoare forma de publicare este sa stocam o referinta intr-un camp
public static, unde orice clasa si thread poate sa-l vada, precum in
urmatoarea figura. Metoda initialize instantiaza un HashSet si il publica
stocand o referinta la el in knownSecrets
public static Set<Secret> knownSecrets;
public void initialize() {
knownSecrets = new HashSet<Secret>();
}

Publicarea unui obiect poate face indirect publicarea altora. Daca adaugi
Secret la multimea knownSecrets, vei publica de asemenea acel secret. In
mod similar, returnarea unei referinte dintr-o metoda non-privata de
asemenea publica obiectul returnat. UnsafeSteates din figura urmatoare
publica(publishes) ceea ce trebuia sa fie un vector de abrevieri privat.
class UnsafeStates {
private String[] states = new String[] {
"AK", "AL" ...
};
public String[] getStates() { return states; }
}

Publicarea starilor in acest fel este problematica, deorece orice apelant


poate sa modifice continutul. In acest caz, vectorul de stare a scapat din
domeniul destinat, deoarece ceea ce trebuia sa fie o stare privata, a fost
facuta efectiv publica.
Publicarea unui obiect publica de asemenea orice obiecte mentionate
de catre campurile sale nonprivate. Mai general, orice obiect care este
accesibil dintr-un obiect publicat urmand un lant/insiruire de referinte catre
campuri nonprivate si apeluri de metode a fost de asemenea publicat.
Din perspectiva unei clase C, o metoda straina(alien) este una al carui
comportament nu a fost complet specificat de C. Aceasta include metode in
alte clase la fel de bine ca metode care pot fi suprascrise(nici private nici
final) in C insusi. Trecerea unui obiect la o metoda straina trebuie de
asemenea considerata ca publicarea acelui obiect. Cat timp nu poti stii ce
cod v-a fi de fapt invocat, nu stii ca metoda straina nu v-a publica obiectul
sau va retine o referinta la el care ar putea fi folosita mai tarziu de alt thread.
Daca un alt fir face de fapt ceva cu o referinta publicata nu are mare
importanta, deoarece riscul de abuz este inca prezent. Odata ce un obiect
scapa/iese(escapes), trebuie sa ne presupunem ca alta clasa sau thread
poate, din neglijenta sau rea intentie, sa abuzeze de el. Acesta este un
motiv intemeiat pentru a utiliza incapsularea : face practic sa analizam
programele pentru corectitudine si face mai greu sa incalcam constrangerire
de proiectare accidental.
Un mecanism final prin care un obiect sau starea sa interna pot fi
publicate, este sa oublicam o instanta a clasei imbricate, asa cum se arata
in ThisEscape din figura urmatoare.
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(
new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
}
}
Cand ThisEscape publica EventListener, implicit publica instantele incluse
in ThisEscape de asemenea, deoarece instantele claselor
imbricate/interioare contin o referinta ascunsa catre instamta imbricata.

3.2.1 Practici de construire sigure(Safe Construction


practices)
ThisEscape ilustreaza un caz special de escape - cand referinta catre this
scapa(escapes) in timpul constructiei. Cand instanta imbricata
EventListener este publicata,la fel este si instanta inclusa in ThisEscape.
Dar un obiect este intr-o stare predictibila, consistenta, doar dupa ce se iese
din constructor, deci publicarea unui obiect din constructor poate publica un
obiect incomplet construit. Acest lucru este adevarat, chiar daca publicarea
este ultima declaratie din constructor.Daca o referinta la this
scapa(escapes) in timpul constructiei, obiectul este considerat neconstruit in
mod corespunzator.
Nu permiteti unei referinte la this sa scape(escape) in timpul
constructiei.

O greseala comuna care poate permite unei referinte la this sa scape


in timpul constructiei este sa pornim un thread din constructor.
Cand un obiect creeaza un thread din constructorul sau, aproape
intotdeauna partajeaza referinta la this cu nout thread, fie explicit ( prin
trecerea la constructor) fie implicit (deoarece Thread sau Runnable este o
clasa imbricata a obiectului care o detine). Noul thread ar putea apoi sa
vada obiectul care il detine inainte de a fi complet construit. Nu este nimic
gresit sa creezi un thread intr-un constructor, dar este cel mai bine sa nu
pornim thread-ul imediat. In schimb sa creem o metoda de initalizare care
porneste thread-ul detinut. (Vezi capitolul 7 pentru mai multe despre
probleme de ciclu de viata ale unui serviciu) . Apelarea unei metode de
instanta care poate fi suprascrisa(una care nu este nici private nici final) din
constructor poate de asemenea sa permina reverintei la this sa
scape(escape).
Daca sunteti tentati sa inregistrati un event listener sau sa porniti un
thread dintr-un constructor, puteti evita construirea necorespunzatoare
folosind un constructor privat si o metoda publica de fabricare, cum se arata
in SafeListener in exemplul urmator:

public class SafeListener {


private final EventListener listener;
private SafeListener() {
listener = new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
};
}
public static SafeListener newInstance(EventSource source) {
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
}

3.3 Thread confinement (Restrangerea firelor de


executie)
Accesarea de date partajate, mutabile necesita folosirea sincronizarii; o
metoda de a evita aceasta cerita este sa nu partajam. Daca datele sunt
accesate doar dintr-un singur thread, nici o sincronizare nu este necesara.
Aceasta tehnica, thread confinement, este una din cele mai simple metode
de a asigura thread-safety. Cand un obiect este restrans(confined) la un
thread, aceasta folosire este automat thread-safe, chiar daca insusi obiectul
restrans nu este.
Swing foloseste restrangearea firelor intens. Componentele vizuale
Swing si obiectele modelului de date nu sunt thread-safe; In schimb,
siguranta este dobandita prin restrangerea lor la firul eveniment expediere
a Swing (to the Swing event dispatch thread).
Pentru a folosi Swing in mod corespunzator, codul care runeaza in alte
thead-uri decat thread-ul eveniment nu ar trebui sa acceseze aceste
obiecte.(Pentru a face acest lucru mai usor, Swing ne ofera mecanismul
invokeLater pentru a programa la executie un Runnable in thread-ul
eveniment). Foarte multe erori in aplicatii Swing sunt cauzate de folosirea
incorecta a acestor obiecte confined(limitate) dintr-un alt thread.
O alta aplicatie comuna a restrangerii/limitarii firelor este folosirea de
obiecte JDBC de conectare. Specificatia JDBC nu necesita ca obiectele
Connection sa fie thread-safe. In aplicatiile server tipice, un thread se
conecteaza dintr-un pool , il foloseste pentru procesarea unui singur
request, apoi il returneaza. Cum majoritatea request-urilor, precum request
de servlet sau apeluri EJB (Enterprise JavaBeans ), sunt procesate sincron
cu un singur thread, si pool-ul nu v-a distribui aceeasi conexiune la un alt fir
pana cand nu a fost returnat, acest pattern de management al conexiunii
implicit limiteaza Connection la acel thread pe durata request-ului.
La fel ca un limbaj care nu are nici un mecanism pentru a forta ca
variabila sa fie pazita de un lacat, ea nu are mijloace de inzolare a unui
obiect la un thread. Restrangerea/limitarea/izolarea firelor este un element
de design de program care trebuie fortat din implementare.
Limbajul si bibliotecile de baza ofera mecanisme care pot ajuta in
mentinerea thread confinement – variabile locale si clasa ThreadLocal – dar
chiar si cu acestea, este inca o responsabilitate a programatorului sa se
asigure ca obiectele thread-confined nu scapa(escape) din thread-ul
destinat.
3.3.1 Ad-hoc thread confinement
Ad-hoc thread confinement descrie cand responsabilitatea pentru
mentinerea thread-confinement cade in intregime pe implementare. Ad-hoc
thread confinement poate fi fragila, deoarece niciuna dintre caracteristicile
limbajului, precum modificatorii de vizibilitate sau variabilele locale, nu ajuta
sa limitam obiectul la thread-ul tinta. De fapt,referinte la obiecte thread-
confined precum componente vizuale sau modele de date in aplicatii GUI
sunt tinute adesea in campuri publice.Decizia de a utiliza izolarea firelor
este de multe ori o consecinta a deciziei de a implementa un subsistem
particular, precum GUI, ca un subsistem single-threaded.Subsistemele
single-threaded pot uneori sa ofere beneficii de simplicitate mai mari decat
fragilitatea ad-hoc thread confinement.
Un caz special de izolare a firelor(thread confinement) se aplica la
variabilele volatile. Este sigur sa efectuam operatii read-modify-write pe
variabile volatile partajate atat timp cat ne asiguram ca variabilele sunt
scrise doar dintr-un singur thread. In acest caz, limitati modificarea unui
singur thread pentru a preveni conditiile de cursa, si vizibilitatea garantata
pentru variabilele volatile asigura ca alte thread-uri vad cea mai updatata
valoare.
Datorita fragilitatii, limitarea ad-hoc a firelor de executie ar trebui
folosita cu grija; daca este posibil folositi o forma mai puternica de limitare a
firelor (thread confinement) in loc (stack confinement sau ThreadLocal)

3.3.2 Stack confinement


Limitarea stivei (stack confinement) este un caz special de limitare a
firelor in care se poate ajunge la un obiect doar prin varibile locale. La fel
cum incapsularea poate face mai usoara conservarea invariantilor,
variabilele locale pot face mai usoara izolarea obiectelor (to confine objects)
la un thread. Variabilele locale sunt intrinsec limitate la thread-ul care se
executa; ele exista cand se executa stiva thread-ului, care nu este
accesibila altor thread-uri. Stack confinement(numita si „in fir” sau „locala
firului”, dar a nu se confunda cu clasa ThreadLocal de biblioteca) is mai
simplu de intretinut si mai putin fragila decat ad-hoc thread confinement.
Pentru variabile locale primitive, precum numPairs in loadTheArk in
figura urmatoare (3.9), nu poti incalca izolarea de stiva (stack confinement),
nici daca ati incerca. Nu este nici o cale pentru a obtine o referinta la
variabile primitive, asa ca semantica limbajului ne asigura ca variabilele
locale promitive sunt mereu stack confined(izolate in stiva).
public int loadTheArk(Collection<Animal> candidates) {
SortedSet<Animal> animals;
int numPairs = 0;
Animal candidate = null;

// animals confined to method, don’t let them escape!


animals = new TreeSet<Animal>(new SpeciesGenderComparator());
animals.addAll(candidates);
for (Animal a : animals) {
if (candidate == null || !candidate.isPotentialMate(a))
candidate = a;
else {
ark.load(new AnimalPair(candidate, a));
++numPairs;
candidate = null;
}
}
return numPairs;
}
Mentinerea stack confinement pentru obiecte referinta necesita putin mai
multa asistenta din partea programatorului pentru a se asigura ca referinta
nu scapa/iese(escape). In loadTheArk, instantiem un TreeSet si stocam o
referinta la animals ale sale.La acest punct, exista exact o referinta la Set,
tinuta in varabila locala, si prin urmre se limiteaza la thread-ul executant.
Totusi,daca ar fi sa publicam o referinta la Set(sau la orice alta componenta
interna), limitarea ar fi incalcata si animalele ar fi scapate/iesite(escaped).
Folosirea unui obiect non-thread-safe intr-un context interiorul firului(within
thread) este inca thread-safe.
Totusi, aveti grija : cerinta de desing ca obiectul sa fie limitat la thread-ul
executant, sau constientizarea ca obiectul limitat nu este thread-safe,
adeseori exista doar in capul programatorului cand codul este scris. Daca
presupunerea de folosire in interiorul firului nu este clar documentata, cei
care vor intretine codul in viitor pot din greseala sa permita obiectului sa
scape(escape) .

3.3.3 ThreadLocal
Un mijloc mai formal de a mentine izolarea firelor(thread confinement) este
ThreadLocal, care iti permite sa asociezi o valoare per-thread cu un obiect
care detine o valoare. ThreadLocal ofera metode accesor get si set care
mentin o copie separata a valorii pentru fiecare thread care le foloseste, asa
ca un get intoarce cea mai recenta valoare trecuta lui set din thread-ul care
se excuta curent.
Variabilele Thread-local sunt adeseori folosite pentru a preveni
partajarea(sharing) in design-uri bazate pe Singleton-uri mutabile sau
variabile globale. De exemplu, o aplicatie single-threaded poate mentine o
conexiune globala la o baza de date care este initializata la inceput pentru a
evita trecerea conexiunii la fiecare metoda. Cum conexiunile JDBC pot sa
nu fie thread-safe, o aplicatie multi-fir care foloseste o conexiune globala
fara coordonare suplimentara nu este nici ea thread-safe. Prin folosirea
ThreadLocal pentru a mentine o conexiune JDBC, precum in
ConnectionHolder in figura urmatoare, fiecare thread va avea propria
conexiune.

private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {


public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
};

public static Connection getConnection() {


return connectionHolder.get();
}

Aceasta tehnica poate fi de asemenea folosita cand o operatie


necesita un obiect temporar cum ar fi un buffer si vrem sa evitam realocarea
obiectului temporar la fiecare invocare. De exemplu, inainte de Java 5.0,
Integer.toString folosea un ThreadLocal penrtru a stoca un buffer 12-biti
pentru formatarea rezultatului, mai degraba decat sa foloseasca un buffer
static partajat (shared static buffer) (care ar necesita blocare/lacate) sau
alocarea unui buffer la fiecare invocare.
Cand un thread apeleaza ThreadLocal.get pentru prima oara,
initialValue este cercetat pentru a oferi valoarea initiala pentru thread.
Conceptual, poti sa te gandesti la un ThreadLocal<T> precum mentinerea
unui Map<Thread,T> care stocheaza valorile specifice threadului, desi acest
lucru nu este modul in care este de fapt implementat. Valorile specifice
thread-ului sunt stocate in Obiectul Thread insusi; cand thread-ul se
termina, valorile specifice thread-ului pot fi garbage collected.
Daca portam o aplicatie single-threaded la un mediu multi-threaded,
putem conserva thread-safety convertind variabilele globale partajate in
ThreadLocals, daca semantica variabilelor partajate permite acest lucru; un
cache la nivel de aplicatie nu ar fi fost la fel de folositor daca ar fi fost
transformat intr-un numar de caches thread-local.
ThreadLocal este folosit la scara larga pentru a implementa
framework-uri de aplicatii. De exemplu containere J2EE asociaza un context
de tranzactie cu un thread care se executa, pe durata unui apel EJB. Acest
lucru este usor implementat folosint un ThreadLocal static care tine un
context de tranzactie : cand codul din framework trebuie sa determine ce
tranzactie se ruleaza curent, el preia contextul tranzactiei din ThreadLocal.
Acest lucru este convenabil in sensul ca reduce nevoia de a trece informatia
de contex executie la fiecare metoda, dar imperecheaza orice cod care
foloseste acest mecanism cu framework-ul.
Este usor sa facem abuz de ThreadLocal prin tratarea thread confinement
propriu ca un mijloc de a folosi variabile globale, sau ca mijloace de a creea
argumente metoda ascunse. La fel ca variabilele globale, variabilele thread-
local pot afecta reusabilitatea si pot introduce imperecheri ascunse (hidden
couplings) intre clase, si ar trebui folosite cu grija.

3.4 Nemutabilitatea(Immutability)
Alte destinatii in jurul nevoii de sincronizare sunt folosirea obiectelor
immutabile (neschimbate). Aproape toate pericolele de atomicitate si
vizibilitate pe care le-am descris pana aici, cum ar fi observarea valorilor
vechi, pierderea din actualizari sau observarea unui obiect de a fi intr-o
stare inconsistenta, au de a face cu cidateniile mai multor fire de executie
care incearca sa acceseze acceasi stare mutabila in acelas timp. Daca
starea unui obiect nu poate fi modificata, aceste riscuri si complexitati dispar
pur si simplu.
Un obiect imutabil este unui a carui stare nu poate fi schimbata dupa
constructie. Obiectele imutabile sunt in mod inerent thread-safe; Invariantii
lor sunt stabiliti in constructor, si starea lor nu se poate schimba,acesti
invarinti se mentin mereu.

Obiectele imutabile sunt intotdeauna thread-safe.

Obiectele imutabile sunt simple. Ele pot fi doar intr-o singura stare, care
este controlata de constructor. Una din cele mai complicate elemente din
design-ul programelor este motivarea/argumentarea despre posibilele stare
ale obiectelor complexe. Pe de alta parte, motivarea starii obiectelor
imutabile este triviala.
Obiectele imutabile sunt de asemenea mai sigure. Trecerea unui
obiect mutabil la cod care nu este de incredere sau publicarea intr-un loc in
care cod care nu este de incredere il poate gasi, este periculoasa, codul
untrusted ii poate modifica starea,sau, mai rau, sa retina o referinta la el si
sa-i modifice starea mai tarziu dintr-un alt thread. Pe de alta parte, obiectele
imutabile nu pot fi subminate in acest mod de cod rau-intentionat sau cu
bugguri, si astfel, acestea sunt in siguranta sa fie partajate si publicate liber,
fara a face copii defensive.
Nici Java Language Specification nici Java Memory Model nu definesc
formal nemutabilitatea,dar nemutabilitatea nu este echivalenta cu
declararea tuturor campurilor unui obiect final. Un obiect ale carui campuri
sunt final,pot fi inca mutabile, deoarece campurile mutabile pot tine referinte
la obiecte mutabile.
Un obiect este imutabil daca :
- Starea sa nu poate fi modificata dupa constructie
- Toate campurile sale sunt finale
- Este construit corespunzator(referinta la el nu scapa(does not escape)
in tipul constructiei
Obiectele imutabile pot inca folosi obiecte mutabile internal pentru a
gestiona starea lor, asa cum este ilustrat de ThreeStooges in exemplul
urmator.In timp ce multimea(Set) care contine numele este mutabila,
design-ul ThreeStooges face imposibil sa modifici multimea(Set) dupa
constructie. Referinta la stooges este finala, astfel incat toata starea
obiectului este atinsa prin field-ul final. Ultima cerinta, constructia
corespunzatoare, este usor indeplinita, atat timp cat codul din constructor
nu face nimic care ar putea sa provoace ca referinta la this sa devina
accesibila codului din afara constructorului sau a apelului catre el.

@Immutable
public final class ThreeStooges {
private final Set<String> stooges = new HashSet<String>();
public ThreeStooges() {
stooges.add("Moe");
stooges.add("Larry");
stooges.add("Curly");
}
public boolean isStooge(String name) {
return stooges.contains(name);
}
}
Pentru ca starea programului se schimba tot timpul, ati putea fi tentati sa va
ganditi ca obiectele immutabile au o intrebuintare limitata, dar acesta nu
este cazul. Este o diferenta intre un obiect care este imutabil si o referinta la
el care este imutabila. Starea programului stocata in obiecte imutabile poate
inca fi updatata inlocuind obiectele imutabile cu o noua instanta care detine
o noua stare; urmatoarea sectiune ofera un exemplu la aceasta tehnica.

3.4.1 Campuri finale


Cuvantul cheie final, o versiune mai limitata a mecanismului const din C++,
suporta constructia obiectelor imutabile. Campurile final nu pot fi
modificate(desi obiectele la care fac referinta pot fi modificabile daca sunt
mutabile), dar au de asemenea o semantica speciala sub Java Memory
Model. Aceasta este utilizarea de campuri finale, care fac posibila
garantarea sigurantei la initializare, care permite obiectelor mutabile sa fie
liber accesate si partajate fara sincronizare.
Chiar daca un obiect este mutabil, sa facem niste campuri final ne simplifica
motivarea despre starea sa, cum limitarea mutabilitatii unui obiect
restrictioneaza multimea de stari posibile. Un obiect care este mai mult
imutabil, dar are una sau doua variabile imutabile este inca mai simplu
decat unul care are multe variabile mutabile. De asemenea, declararea
campurilor final ii documenteaza pe cei ce intretin codul ca aceste campuri
nu au de gand sa se schimbe.
Asa cum este o buna practica sa faci toate campurile private daca nu ai
nevoie de vizibilitate, la fel este o buna practica sa faci toate campurile final
daca nu ai nevoie sa fie mutabile.

3.4.2 Exemplu : folosirea de variabile volatile pentru a


publica obiecte imutabile
In UnsafeCachingFactorizer la pagina 24, am incercat sa folosim douna
variabile AtomicReferences pentru a stoca ultimul numar si ultimii factori,
dar acest lucru nu este thread-safe deoarece nu putem prinde si updata
cele 2 variabile aferente intr-un mod atomic. Folosirea de variabile volatile
pentru aceste valori nu ar fi thread-safe din acelasi motiv. Totusi, obiecte
imutabile pot uneori sa ofere o forma slaba de atomicitate.
Servletul factorizant(factoring) executa doua operatii care trebuie sa fie
atomice: updatarea rezultatului cached si preluarea conditionala a factorilor
in cazul in care numarul cached este egal cu numarul din request.
Ori de cate ori un grup de elemente trebuie sa se comporte atomic, ar
trebui sa luam in considerare creearea unei clase container, imutabila
pentru ele cum ar fi OnaValueCache in figura urmatoare.
Conditiile de cursa in accesarea sau updatarea a multiple variabile in
relatie pot fi eliminate prin folosirea unui obiect imutabil care sa tina toate
variabilele.

@Immutable
class OneValueCache {
private final BigInteger lastNumber;
private final BigInteger[] lastFactors;
public OneValueCache(BigInteger i,BigInteger[] factors) {
lastNumber = i;
lastFactors = Arrays.copyOf(factors, factors.length);
}
public BigInteger[] getFactors(BigInteger i) {
if (lastNumber == null || !lastNumber.equals(i))
return null;
else
return Arrays.copyOf(lastFactors, lastFactors.length);
}
}
Cu un obiect mutabil container, ar trebui sa folosim blocarea/lacatele pentru
a asigura atomicitatea. Cu unul imutabil, odata ce un thread capata o
trimitere la acesta, nu trebuie niciodata sa ne facem griji despre un alt
thread care ii modifica starea. Daca variabilele sunt pe cale sa fie updatate,
un nou obiect container v-a fi creeat, dar orice thread in lucru cu containerul
precedent il v-a vedea intr-o stare consistenta.
VolatileCachedFactorizer in exemplul urmator foloseste un obiect
OneValueCache pentru a stoca numarul cached si factorii. Cand un thread
seteaza campul cache volatil la o referinta la un nou obiect
OneValueCache, noile date cached devin imediat vizibile la alte thread-uri.
Operatiile legate de cache nu pot interfera una cu cealata deoarece
ValueCache este imutabil si campul cache este accesat doar odata un
fiecare bucata de cod relevanta. Aceasta combinare a unui obiect container
imutabil pentru mai multe variabile de stare legate de un invariant, si o
referinta volatila folosita pentru a asigura vizibilitatea in timp, permite lui
VolatileCachedFactorizer sa fie thread-safe, chiar daca nu foloseste
blocarea explicita.
@ThreadSafe
public class VolatileCachedFactorizer implements Servlet {
private volatile OneValueCache cache = new OneValueCache(null, null);
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = cache.getFactors(i);
if (factors == null) {
factors = factor(i);
cache = new OneValueCache(i, factors);
}
encodeIntoResponse(resp, factors);
}
}

3.5 Publicarea in conditii de siguranta(safe publication)


Pana acum ne-am concentrat pe garantarea ca un obiect nu v-a fi
publicat, de exemplu atunci cand trebuie limitat(to be confined) la un thread
sau in interiorul altui obiect. Bineinteles, sunt situatii cand vrem sa partajam
un obiect intre threaduri, si in acest caz trebuie sa o facem sigur. Din
pacate, simpla stocare a unei referinte la un obiect intr-un camp public ,
precum in 3.14, nu este suficienta pentru a publica obiectul safely.

Listing 3.14. Publishing an object without adequate synchronization. Don’t do this.

//Unsafe publication
public Holder holder;

public void initialize() {


holder = new Holder(42);
}

Puteti fi surprinsi despre cat de rau poate esua acest exemplu infensiv-
aparent. Din cauza problemelor de vizibilitate, Holder poate aparea in alt
thread ca fiind intr-o stare inconsistenta, chiar daca invariantii sai au fost
stabiliti corect in constructor. Aceasta publicare necorespunzatoare poate
permite unui alt fir sa observe un obiect partial construit.

3.5.1 Publicare necorespunzatoare : atunci cand


obiectele bune merg prost
Nu ne putem baza pe integritatea obiectelor construite partial. Un
thread observator poate vedea un obiect intr-o stare inconsistenta, iar apoi
poate vedea ca starea s-a schimat dintr-o data, chiar daca nu a fost
modificat de la publicare(since publication).De fapt, in cazul in care
Holder(detinatorul) din figura 3.15 este publicat folosind publicarea
nesigura(unsafe publication) din 3.14, si un thread altul dacet cel publicant
ar apela assertSanity, ar putea „arunca” AssertionError. (The problem here
is not the Holder class itself, but that the Holder is not properly published.
However, Holder can be made immune to improper publication by declaring
the n field to be final, which would make Holder immutable; see Section
3.5.2.)

Listing 3.15. Class at risk of failure if not properly published.


Because

public class Holder {


private int n;

public Holder(int n) { this.n = n; }

public void assertSanity() {


if (n != n)
throw new AssertionError("This statement is false.");
}
}

Deoarece nu a fost folosita sincronizarea pentru a face Holder vizibil la


alte threaduri, spunem ca Holder nu a fost publicat in mod corespunzator.
Doua lucruri pot merge prost cu obiectele publicate incorect. Alt thread
poate vedea o valoare invechita pentru campul container(holder), si astfel
pot vedea o referinta nula sau o alta valoare mai mare, chiar daca o valoare
a fost plasata in container(holder). Dar, si mai rau, alte thread-uri pot vedea
o valoare updatata pentru referinta container, dar valori invechite pentru
valorile de stare din Holder.
Pentru a face lucrurile si mai putin previzibile, un fir poate vedea o valoare
veche pentru prima data cand citeste un camp si o valoare mai updatata
data urmatoare, care este motivul pentru care assertSanity poate arunca o
AssertionError.
Cu riscul de a ne repeta, lucruri foarte stranii se pot intampla cand
datele sunt partajate(shared) intre thread-uri fara suficienta sincronizare.

3.5.2 Obiectele imutabile si siguranta initializarii


Deoarece obiectele imutabile sunt atat de importante, Java Memory
Model ofera o garantie a initializarii in siguranta pentru partajarea obiectelor
imutabile. Asa cum am vazut, daca referinta la un obiect devine vizibila la
un alt thread nu inseamna ca si starea acelui obiect este vizibila threadului
consumator. Pentru a garanta o observare consistenta asupra starii unui
obiect, este necesara sincronizarea.
Obiectele imutabile, pe de alta parte, pot fi accesate in siguranta chiar
si atunci cand sincronizarea nu este folosita pentru a publica referinta la
obiect(even when synchronization is not used to publish the object
reference.). Pentru a avea aceasta garantie a initalizarii sigure, toate
conditiile pentru imutabilitate trebuie indeplinite : stare nemodificabila, toate
campurile finale, si constructie corespuzatoare. (Daca Holder din 3.15 era
imutabil, assertSanity nu putea sa arunce AssertionError, chiar daca Holder
nu ar fi fost publicat corespunzator)
Obiectele imutabile pot fi folosite in siguranta in orice thread fara
sincronizare aditionala, chiar atunci cand sincronizarea nu este folosita
pentru a le publica.
Aceasta garantie se extinde la valorile din toate campurile final ale
obiectelor construite corespunzator; campurile final pot fi accesate in
siguranta fara sincronizare aditionala. Totusi, in cazul in care campurile final
fac referinta la obiecte mutabile, sincronizarea este inca necesara pentru a
accesa starea obiecteler la care fac referire.

3.5.3 Expresii de publicare sigura


Obiectele care nu sunt imutabile trebuie sa fie publicate in siguranta(safely
published), ceea ce presupune de obicei sincronizarea atat a firului care
publica cat si a firului care consuma. Pentru moment, sa ne concentram pe
garantarea ca firul cosumator poate vedea obiectul in starea in care a fost
publicata.

Pentru a publica un obiect in siguranta, atat referinta la obiect cat si


starea obiectului trebuie sa fie vizibile la alte thread-uri in acelasi timp. Un
obiect construit corespunzator poate fi publicat in siguranta prin :
- Iniatizarea unei referinte la un obiect dintr-un inializer static
- Prin stocarea unei referinte la el intr-un camp volatil sau
AtomicReference
- Prin stocarea la o referinta la el intr-un camp final al unui obiect
construit corespunzator
- Prin stocarea unei referinte la el intr-un camp care este pazit
corespunzator de un lacat

Sincronizarea interna in colectiile thread-safe insemnand plasarea unui


obiect intr-o colectie thread-safe, precum un Vector sau un
synchornizedList,indeplineste ultima din aceste cerinte. Daca threadul A
plaseaza un obiect X intr-o colectie thread-safe si thread-ul B il preia
ulterior, B are garantia sa vada starea lui X asa cum A a lasat-o, chiar daca
codul aplicatiei care se ocupa de X in acest mod, nu are sincronizare
explicita.
Coletiile de librarii thread-safe ofera urmatoarele garantii ale publicarii
sigure, chiar daca documentatia din Java nu este destul de clara pe
marginea acestui subiect :
- Plasarea unei chei sau valori intr-un HashTable,synchronizedMap sau
ConcurrentMap publica in siguranta la orice thread care le preia din
Map (in mod direct sau printr-un iterator)
- Plasarea unui element intr-un Vector,CopyOnWriteArrayList ,
CopyOnWriteArraySet,synchronizedList sau synchronizedSet publica
in siguranta la orice thread care le preia din colectie
- Plasarea unui element intr-un BlockingQueue sau
ConcurrentLinkedQueque publica in siguranta la orice thread care le
preia din coada
Alte mecanisme de transfer in legatura cu biblioteca de clase (cum ar fi
Future si Exchanger) constituie de asemenea publicare in conditii de
siguranta; vom identifica acestea ca oferind publicare sigura cand vor fi
introduse.
Folosirea unui initializator static este adesea cea mai usoara si sigura
cale de a publica obiecte care se pot construi static
public static Holder holder = new Holder(42);

Initializatorii statici sunt executati de JVM la initializarea claselor; din cauza


sincronizarii interne a JVM, acest mecanism garanteaza publicarea in
siguranta a obiectelor intializate in acest mod.

3.5.4 Obiecte efectiv imutabile


Publicarea in siguranta este suficienta pentru ca alte threaduri sa
acceseze in siguranta obiecte care nu vor fi modificate dupa publicare fara
sicronizare aditionala. Toate mecanismele de publicare in siguranta
garanteaza ca starea cum a fost publicata a unui obiect este vizibila tuturor
threadurilor care o acceseaza de indata ce referinta la aceasta este vizibila,
si in cazul in care starea nu v-a fi schimbata din nou, acest lucru este
suficient pentru a asigura ca orice acces este sigur.
Obiectele care nu sunt tehnic imutabile, dar ale caror stare nu se
modifica dupa publicare, sunt numite effectiv imutabile(effective immutable).
Ele nu necesita sa indeplineasca definitia stricta a imutabilitatii din sectiunea
3.4; ei au nevoie doar sa fie treatate de program ca si in cazul in care
acestea au fost imutabile dupa ce acestea sunt publicate. Folosind efectiv
obiecte imutabile poate simplifica developmentul si creste performanta prin
reducerea nevoii de sincronizare.
Obiectele publicate in siguranta efectiv imutabile pot fi folosite in siguranta
de orice thread fara sincronizare aditionala.
De exemplu Date este mutabil, dar daca il folosesti ca si cum ar fi
imutabil, poti fi capabil sa elimini blocarea care altfel ar fi fost necesara cand
partajam un obiect Date intre threaduri.
Sa presupunem ca vrem sa mentinem un Map care sa stocheze
ultimul timp de login al fiecarui user.
Public Map<String,Date> lastLogin =
Collections.synchronizedMap(new HashMap<String, Date>());
Daca valorile Date nu sunt modificate dupa ce au fost plasate in Map, atunci
sincronizarea in implementarea synchronizedMap este suficienta pentru a
publica valorile Date in siguranta, si nici o sincronizare aditionala nu este
necesara cand le accesam.

3.5.5 Obiecte mutabile


Daca un obiect poate fi modificat dupa constructie, publicarea in siguranta
asigura doar vizibilitatea starii cum a fost publicata. Sincronizarea trebuie
folosita nu doar pentru a publica obiecte mutabile, dar de asemenea pentru
a asigura vizibilitatea in modificarile ulterioare ale obiectului, de fiecare data
cand este accesat. Pentru a partaja un obiecte mutabile in siguranta, ele
trebuie sa fie publicate in siguranta si sa fie thread-safe sau pazite de un
lacat.
Cerintele de publicare pentru un obiect depinde de mutabilitatea sa :
- Obiectele imutabile pot fi publicate prin orice mecanism
- Obiectele efectiv imutabile trebuie publicate in siguranta
- Obiectele mutabile trebuie sa fie publicate in siguranta si sa fie atat
thread-safe sau pazite de un lacat

3.5.6 Partajarea in siguranta a obiectelor


Ori de cate ori ati primit o referinta la un obiect, trebuie sa stiti ce este
permis sa faceti cu el. Trebuie sa obtineti lacatul inainte de a-l folosi ? Aveti
permisa modificarea starii sau doar citirea ? Foarte multe erori de
concurenta provin din incapacitatea de a intelege aceste norme de
angajament pentru obiecte partajate.
Cand publici un obiect, trebuie sa documentezi cum poate fi accesat
onbiectul.
Cele mai folositoare politici de folosire si partajare a obiectelor in
programele concurente sunt :
- Thread-confined. Un obiect thread-confined este detinut exclusiv si
limitat la un thread, si poate fi modificat doar de thread-ul care il detine
- Shared read-only. Un obiect partajat numai pentru citire poate fi
accesat concurent de mai multe threaduri fara sincronizare aditionala,
dar nu poate fi modificat de nici un thread. Obiectele shared read-only
includ obiectele imutabile si efectiv imutabile.
- Shared thread-safe . Un obiect thread-safe efectueaza sincronizarea
intern, astfel incat multiple threaduri pot sa-l acceseze fara
sincronizare viitoare prin interfata sa publica.
- Guarded. Un obiect pazit poate fi accesat doar prin lacatul specific.

Capitolul 4 : compunerea obiectelor


Capitolul 14 Construirea sicronizatorilor custom
(Building custom synchronizers)

Librariile de clasa includ un numar de clase dependete de stare –


acestea avand operatiuni cu preconditii bazate pe stare, cum sunt
FutureTask, Semaphore si BlockingQueque. De exemplu, nu poti scoate un
element dintr-o coada goala sau sa primesti rezultatul unui task care nu a
fost inca finalizat; inainte ca aceste operatii sa poata continua, trebuie sa
astepti pana cand coada intra intr-o stare nonempty sau task-ul intra intr-o
stare „completa”.
Cel mai usor mod de a construi clase dependente de stare este de
obicei sa construim pe clasele dependente de stare pe care le avem deja;
Am facut acest lucru in ValueLatch de la pagina 187, folosind
CountDownLatch pentru a oferi comportamentul blocant cerut.
Dar in cazul in care clasele din librarii nu ofera functionalitatea care ne este
necesara, putem sa ne construim proprii sincronizatori(synchronizers),
folosind mecanismul de nivel jos(low-level) oferit de limbaj si de librarii,
incuzand cozile de conditie intrinseci (intrinsic condition queues), obiecte
Condition explicite, si frameworkul AbstractQueuedSynchronizer. Acest
capitol exploreaza diferitele optiuni de punere in aplicare a dependentei de
stare si normele de folosire a mecanismelor de dependeta de stare oferite
de platforma.
14.1 Gestionarea dependentei de stare
Intr-un program cu un singur fir de executie, in cazul in care o conditie
prealabila bazata pe stare(cum ar „pool-ul de conexiuni nu este gol”) nu se
indeplineste cand o metoda este apelata, nu va deveni niciodata adevarata.
Prin urmare, clasele din programele secventiale pot fi codate sa esueze
cand preconditiile nu sunt indeplinite. Dar in programele concurente,
conditiile bazate pe stare se pot schimba prin actiunile altor fire : un pool
care era gol acum cateva instructiuni, poate deveni non-empty deoarece alt
thread a returnat un element. Metodele dependente de stare pe obiecte
concurente pot uneori sa esueze cand preconditiile nu sunt indeplinite, dar
exista de multe ori o alternativa mai buna: asteptatarea ca preconditia sa
devina adevarata.
Operatiile dependente de stare care se blocheaza pana cand
operatiunea poate continua sunt mai convenabile si mai putin predispuse la
erori decat cele care pur si simplu nu reusesc.

Conditia incorporata in mecanismul cozii(condition queue mechanism)


permite thread-urilor sa se blocheze pana cand un obiect a intrat in starea
care ii permite sa progreseze si sa trezeasca(wake) thread-uri blocate cand
acestea pot fi in masura sa faca progrese suplimentare.
Vom acoperi detaliile conditiilor din coada in sectiunea 14.2, dar pentru a
motiva valoarea unui mecanism condition wait, vom arata mai intai cum
dependenta de stare poate fi (extrem de dureroasa) abordata folosind
polling(interogarea) si sleeping(adormirea).
O actiune blocanta dependenta de stare ia forma aratata in figura 14.1.
Patternul blocarii este oarecum neobisnuit prin faptul ca lacatul este eliberat
si reprimit in mijlocul unei operatii. Variabilele de stare care compun
preconditia trebuie pazite de lacatul unui obiect, astfel incat sa ramana
constante pe perioada cand preconditia este testata. Dar daca preconditia
nu este indeplinita, lacatul trebuie eliberat astfel incat un alt thread sa poata
modifica starea obiectului, altfel, preconditia nu va deveni nicioadata
adevarata. Lacatul trebuie reprimit inainte sa se faca testarea preconditiei
din nou.
Listing 14.1. Structure of blocking state-dependent actions.
acquire lock on object state
while (precondition does not hold) {
release lock
wait until precondition might hold
optionally fail if interrupted or timeout expires
reacquire lock
}
perform action
release lock

Buffere delimitate, cum ar fi ArrayBlockingQueue sunt utilizate in mod


obisnuit in designul producator-consumator. Un buffer delimitat ofera
operatii put si take, fiecare din ele avand preconditii : nu poti sa iei un
element dintr-un buffer gol, nu poti sa pui un element intr-un buffer plin.
Operatiile dependente de stare pot avea de-a face cu preconditii care
esueaza, aruncand o exeptie sau returnand un status de eroare( facand
problema a apelantului), sau blocandu-se pana cand un obiect intra in
starea care trebuie.
Avem de gand sa developam mai multe implementari ale unui buffer
delimitat, care abordeaza diferit esuarea preconditiei.
Fiecare extinde BaseBoundedBuffer din 14.2, care implementeaza un buffer
circular bazat pe vector unde variabilele de stare ale bufferului (buf, head,
tail si cout) sunt pazite de lacatul intrinsec al bufferului). Ne ofera metode
sychronized doPut si doTake care sunt folosite in subclase pentru a
implementa operatiile put si take; starea care sta la baza este ascunsa de
subclase.

14.1.1 Exemplu : propagarea esuarii preconditiilor la


apelanti
GrumpyBoundedBuffer (grumpy=morocanos) in 14.3 este o prima
incercare bruta in implementarea unui buffer delimitat. Metodele put si take
sunt synchronized pentru a asigura acces exclusiv la starea bufferuluil, cum
ambele implica logica check-then-act in accesarea bufferului.
Cat timp aceasta abordare este destul de usor de implementat, este
enervant de folosit. Exceptiile sunt menite pentru conditii exceptionale.
„Bufferul nu e plin” nu este o conditie exceptionala pentru un buffer delimitat
mai mult decat „rosu” este o conditie exceptionala pentru un semnal de
trafic.
Simplificarea in implementarea bufferului (fortand apelantul sa gestioneze
dependenta de stare), este mai mult decat facuta din cauza complicatiei
substantiale in folosirea ei, deoarece acum apelantul trebuie sa fie pregatit
sa prinda exceptii(catch exceptions) si eventual sa incerce din nou pentru
fiecare operatie din buffer.Un apel bine-structurat la take este prezentat in
codul din figura urmatoare - nu foarte frumos, in special daca put si take
sunt apelate pe tot parcursul programului.
Listing 14.4. Client logic for calling GrumpyBoundedBuffer.
while (true) {
try {
V item = buffer.take();
// use item
break;
} catch (BufferEmptyException e) {
Thread.sleep(SLEEP_GRANULARITY);
}
}
Listing 14.2. Base class for bounded buffer implementations.
@ThreadSafe
public abstract class BaseBoundedBuffer<V> {
@GuardedBy("this") private final V[] buf;
@GuardedBy("this") private int tail;
@GuardedBy("this") private int head;
@GuardedBy("this") private int count;
protected BaseBoundedBuffer(int capacity) {
this.buf = (V[]) new Object[capacity];
}
protected synchronized final void doPut(V v) {
buf[tail] = v;
if (++tail == buf.length)
tail = 0;
++count;
}
protected synchronized final V doTake() {
V v = buf[head];
buf[head] = null;
if (++head == buf.length)
head = 0;
--count;
return v;
}
public synchronized final boolean isFull() {
return count == buf.length;
}
public synchronized final boolean isEmpty() {
return count == 0;
}
}

Listing 14.3. Bounded buffer that balks when preconditions are not met.
@ThreadSafe
public class GrumpyBoundedBuffer<V> extends BaseBoundedBuffer<V> {
public GrumpyBoundedBuffer(int size) { super(size); }
public synchronized void put(V v) throws BufferFullException {
if (isFull())
throw new BufferFullException();
doPut(v);
}
public synchronized V take() throws BufferEmptyException {
if (isEmpty())
throw new BufferEmptyException();
return doTake();
}
}
O varianta a acestei abordari(14.4) este sa returnam un cod de eroare cand
bufferul este in stare incorecta. Acesta este o imbunatatire minora in faptul
ca nu abuzeaza de mecanismul de exceptii aruncand o eroare care chiar
inseamna „imi pare rau, mai incercati o data”, dar nu trateaza o problema
fundamentala : apelantii trebuie sa se ocupe de esecurile preconditiilor ei
insisi.
Codul client din 14.4 nu este singurul mod de a implementa logica „mai
incearca”. Apelantul poate reincerca apelul la take imediat, fara sa adoarma
- o abordare numita asteptarea ocupata(busy waiting) sau asteptarea de
rotatie (spin waiting). Aceasta ar putea consuma destul de mult timp CPU
daca starea bufferului nu se schimba o perioada. Pe de alta parte, daca
apelantul decide sa doarma pentru a nu consuma atat de mult timp CPU, ar
putea sa dorma prea mult (oversleep) daca starea bufferului se schimba
dupa apelul la sleep. Deci codului client ii ramane o varianta dintre folosirea
slaba a CPU si responsivitatea slaba a adormirii. (Undeva intre asteptarea
ocupata(busy waiting) si adormire(sleeping) ar trebui apelat Thread.yield in
fiecare iteratie, care este un indiciu pentru planificator ca ar fi un timp
rezonabil sa lase alt thread sa ruleze). Daca astepti ca un alt thread sa faca
ceva, acel ceva s-ar putea intampla mai repede, daca cedezi procesorul,
mai degraba decat consumand cuanta completa a planificatorului.

14.1.2 Exemplu : blocarea bruta prin polling si sleeping


SleepyBoundedBuffeer in fig 14.5 incearca sa scuteasca apelantii de
inconvenienta implementarii logicii de reincercare la fiecare apel prin
incapsularea aceluiasi mecanism brut de reincercare „poll and sleep” in
interiorul operatiilor put si take. Daca bufferul este gol, take adoarme pana
cand un alt thread pune date in buffer; daca bufferul e plin, put adoarme
pana cand un alt thread face spatiu prin scoaterea unor date. Aceasta
abordare incapsuleaza management-ul preconditiilor si simplifica folosirea
bufferului- in mod clar un pas in directia potrivita.
Implementarea lui SleepyBoundedBuffer este mai complicata decat
ultima incercare. Codul din buffer trebuie sa testeze conditia de stare
apropiata cu lacatul bufferului tinut. Daca testul esueaza, threadul care se
executa adoarme pentru un timp, mai intai eliberand lacatul astfel incat alte
thread-uri sa acceseze bufferul. Odata ce thread-ul se trezeste, recapata
lacatul si incearca din nou,alternand intre adormire si testarea conditiei de
stare pana cand operatia poate continua.
Din perspectiva apelantului, acesta merge bine – daca operatia poate
continua imediat, ea continua, altfel, se blocheaza – si apelantul nu are de-a
face cu mecanica esuarii si reincercarii. Alegerea granularitatii adormirii este
un compromis intre responsivitate si folosirea CPU; cu cat este mai mica
granularitatea adormirii, cu atat este mai responsiv, dar de asemenea mai
multe resurse CPU consumate. Figura 14.1 arata cum granularitatea
dormirii poate afecta responsivitatea: poate fi un delay intre timpul cand
bufferul devine disponibil si timpul cand threadul se trezeste si
verifica/testeaza din nou.

Listing 14.5. Bounded buffer using crude blocking.

@ThreadSafe
public class SleepyBoundedBuffer<V> extends BaseBoundedBuffer<V> {
public SleepyBoundedBuffer(int size) { super(size); }
public void put(V v) throws InterruptedException {
while (true) {
synchronized (this) {
if (!isFull()) {
doPut(v);
return;
}
}
Thread.sleep(SLEEP_GRANULARITY);
}
}
public V take() throws InterruptedException {
while (true) {
synchronized (this) {
if (!isEmpty())
return doTake();
}
Thread.sleep(SLEEP_GRANULARITY);
}
}
}

SleepyBoundedBuffer de asemenea creeaza o alta cerinta pentru


apelant : abordarea InterruptedExeption. Cand o metoda se blocheaza
asteptand ca o conditie sa fie adevarata, cel mai bun lucru de facut este
mecanismul de anulare(vezi capitolul 7). La fel ca majoritatea metodelor
blocante de librarie care se comporta bine, SleepyBoundedBuffer suporta
anularea prin intrerupere, intorcandu-se mai repede si aruncand o exceptie
InterruptedException daca a fost intrerupta.
Aceste incercari de a sintetiza o operatie de blocare din polling si
sleeping au fost foarte dureroase. Ar fi mai bine sa avem o metoda de a
suspenda un thread, dar sa ne asiguram ca se trezeste prompt cand o
conditie (cum ar fi bufferul sa nu fie plin) devine adevarata. Acest lucru este
exact ceea ce fac cozile de conditii(condition queues).

14.1.3 Cozile de conditii(condition queues) pentru


salvare
Cozile de conditii sunt precum clopotelul „painea prajita este gata” al
prajitorului de paine. Daca asculti la el, esti notificat prompt cand painea
prajita este gata si poti sa renunti la ceea ce faceai (sau nu, poate vrei sa
termini de citit ziarul intai) si sa-ti iei painea prajita. Daca nu asculti la el, poti
sa pierzi notificarea , dar la reintoarcearea in bucatarie poti observa starea
prajitorului si fie sa iti iei painea daca este gata, fie sa incepi din nou
ascultatul clopotelului, daca nu a sunat inca.
O condition queue(coada de conditii) primeste numele pentru ca ofera
unui grup de threaduri - apelat de o multime wait – o cale de a astepta ca o
conditie specifica sa devina adevarata. Spre deosebire de cozile tipice in
care elementele sunt date, elementele unei condition queue sunt thread-
urile care asteapta dupa conditie.
La fel cum fiecare obiect Java se poate comporta ca un lacat, fiecare
obiect se poate comporta ca o condition queue(coada de conditii), si
metodele wait, notify si notifyAll ale Object constituie API-ul pentru cozile de
conditii intrinseci. Un lacat intrinsec al unui obiect si coada de conditii
intrinseca sunt relationate : pentru a apela orice metoda a cozii de conditii a
obiectului X, trebuie sa detii lacatul obiectului X. Aceasta este deoarece
mecanismul de asteptare pentru conditiile bazate pe stare(state-based) este
in mod necesar strans legat de mecanismul pentru conservarea consistentei
starii: nu poti astepta dupa o conditie daca nu poti examina starea, si nu poti
elibera alt thread dintr-o asteptare de conditie decat daca poti sa-i modifici
starea.
Object.wait elibereaza atomic lacatul si cere sistemului de operare sa
suspende thread-ul curent, permitand altor thread-uri sa primeasca lacatul
si sa modifice starea obiectului. La trezire, el isi reachizitioneaza lacatul
inainte de a reveni. In mod intuitiv, apelarea wait inseamna : „ Vreau sa
adorm, dar trezeste-ma cand ceva interesant se intampla”, si apelarea notify
inseamna „ceva interesant s-a intamplat”.
BoundedBuffer in fig 14.6 implementeaza un buffer delimitat folosind
wait si notifyAll. Acesta este mai simplu decat versiunea cu sleeping, si este
atat mai eficient(se trezeste mai putin frecvent daca starea bufferului nu se
schimba) cat si mai responsiv (se trezeste prompt daca o schimbare
interesanta de stare se intampla) . Acesta este o mare imbunatatire, dar
puteti observa ca introducerea condition queues nu a schimbat semantica
comparativ cu versiunea sleeping. Este pur si simplu o optimizare pe mai
multe planuri : eficienta CPU, context-switch overhead, si responsivitate.
Condition queues nu te lasa sa faci ceva ce nu se poate face cu sleeping si
polling, dar ele fac mult mai usora si mai eficienta exprimarea si gestiunea.

Listing 14.6. Bounded buffer using condition queues.

@ThreadSafe
public class BoundedBuffer<V> extends BaseBoundedBuffer<V> {
// CONDITION PREDICATE: not-full (!isFull())
// CONDITION PREDICATE: not-empty (!isEmpty())
public BoundedBuffer(int size) { super(size); }
// BLOCKS-UNTIL: not-full
public synchronized void put(V v) throws InterruptedException {
while (isFull())
wait();
doPut(v);
notifyAll();
}
// BLOCKS-UNTIL: not-empty
public synchronized V take() throws InterruptedException {
while (isEmpty())
wait();
V v = doTake();
notifyAll();
return v;
}
}

BoundedBuffer este in final suficient de bun pentru a fi folosit – este usor sa


folosesti si sa gestionezi dependenta de stare. O versiune de productie ar
trebui de asemenea sa includa versiuni cronometrate ale put si take, astfel
incat operatiunile de blocare se pot bloca in cazul in care nu se incadreaza
intr-un anumit timp. O versiune cronometrata/temporizata a Object.wait face
acesta sa se puna usor in aplicare.
14.2 Folosirea condition queues
Condition queues fac mai usoara construirea eficienta si responsiva a
claselor dependete de stare; dar ele inca pot fi usor folosite incorect, sunt o
multime de reguli cu privire la utilizarea lor corespuzatoare, care nu sunt
fortate de compilator sau platforma.(Acesta este unul din motivele de a
construi pe clase precum LinkedBlockingQueue, CountDownLatch,
Semaphore si FutureTask cand putem; daca te descurci in acest mod, este
mult mai usor).

14.2.1 Predicatul conditie


Cheia pentru folosirea corecta a condition queues este identificarea
predicatelor conditie pe care obiectul poate astepta. Este predicatul conditie
care provoaca multa confuzie in jurul wait si notify, deoarece nu are nici o
instantiere API si nimic nu asigura utilizarea corecta in language
specification sau in implementarea JVM. De fapt nu este mentionat direct
deloc nici in language specification nici in Javadoc. Dar fara aceasta,
asteptarea conditionata(condition waits) nu ar functiona.
Predicatul de conditie(condition predicate) este preconditia care face o
operatie dependenta de stare in primul rand. Intr-un bounded buffer ,take
poate continua doar daca bufferul nu e gol; altfel, trebuie sa astepte. Pentru
take condition predicate este „bufferul nu este gol”, pe care take trebuie sa o
testeze inainte sa continue. In mod similar, condition predicate pentru put
este”bufferul nu este plin”. Condition predicates sunt expresii construite din
variabilele de stare ale clasei. BaseBoundedBuffer testeaza „bufferul nu
este gol” comparand count cu zero, si testeaza „bufferul nu este plin”,
comparand count cu marimea bufferului (buffer’s size).

Documentati condition predicate(s) asociate cu o condition queue si


operatiile care asteapta dupa ele.

Exista o relatie importanta 3 cai intr-un condition wait(asteptare


conditionata) care implica blocarea/lacatul, metoda wait, si predicatul
conditie(condition predicate).
Condition predicate implica variabile de stare, si variabilele de stare sunt
pazite de un lacat,asa ca inainte de a testa condition predicate, trebuie sa
obtinem lacatul asociat. Obiectul lacat si obiectul condition queue (obiectul
pe care wait si notify sunt invocate) trebuie de asemenea sa fie acelasi
obiect.
In BoundedBuffer, starea bufferului este pazita de lacatul bufferului si
obiectul buffer este folosit coada de conditii(condition queue). Metoda take
primeste lacatul bufferului si apoi testeaza predicatul conditie (condition
predicate) (bufferul este gol). Daca bufferul este cu adevarat nevid, metoda
take elimina primul element, ceea ce se poate face pentru ca inca detine
lacatul care pazeste starea bufferului.
Daca predicatul conditie nu este adevarat(bufferul este gol), take
trebuie sa astepte ca un alt thread sa puna un obiect in buffer. Ea face
acest lucru apeland wait pe condition queue intrinsec(coada de conditie
intrinseca) al bufferului, ceea ce necesita sa tina lacatul pe obiectul
condition queue. Cum ar trebui sa fie design-ul atent, take deja detine acel
lacat, care este necesar sa testeze predicatul conditie(si daca predicatul
conditie este adevarat, sa modifice starea bufferului in acceasi operatie
atomica). Metoda wait elibereaza lacatul, blocheaza thread-ul curent si
asteapta pana cand timeoutul specificat expira, thread-ul este intrerupt , sau
thread-ul este trezit de o notificare. Dupa ce thread-ul se trezeste , wait
reprimeste lacatul inainte de a se intoarce. Un thread care se trezeste din
wait nu primeste nici o prioritate speciala in reprimirea lacatului; se sustine
pentru blocare precum orice alt thread care incearca sa intre intr-un bloc
synchronized.

Fiecare apel la wait este implicit asociat cu un predicat conditie specific.


Cand apelarea lui wait cu privire la o conditie predicat particulara, apelantul
trebuie deja sa tina lacatul asociat cu condition queue, si acel lacat trebuie
de asemenea sa pazeasca variabilele de stare din care predicatul conditie
este compus.

14.2.2 Trezirea prea devreme (Waking up too soon)


Ca si cand relatia 3 cai dintre lacat, starea predicatului si condition
queue nu ar fi fost destul de complicata, acel return din wait nu inseamna in
mod necesar ca predicatul conditie pe care thread-ul il asteapta a devenit
adevarat.
O singura condition queue intrinseca ar putea fi folosita cu mai mult
decat un predicat conditie. Cand threadul tau este trezit deoarece cineva a
apelat notifyAll, nu inseamna neaparat ca predicatul conditie pe care
asteptai este acum adevarat (Este ca si cum ai partaja un singur clopotel
intre toaster si filtrul de cafea; cand acesta suna, trebuie sa te uiti care
aparat a sunat semnalul.). In mod aditional, wait are permis sa se intoarca
spuriosly – nu in raspuns la orice thread care a apelat notify.
Cand controlul este reluat de codul care a apelat wait, acesta
reprimeste lacatul asociat cu condition queue. Este acum predicatul conditie
adevarat ? Poate. Ar fi putut sa fie adevarat la momentul cand thread-ul
care a notificat a apelat notifyAll, dar ar fi putut sa devina fals din nou pana
in momentul in care se reprimeste lacatul. Alte thread-uri ar fi putut obtine
lacatul si schimba starea obiectului intre timpul in care thread-ul s-a trezit si
timpul in care wait a reprimit lacatul. Ori poate nu a fost adevarat niciodata
pana ai apelat wait. Nu poti stii de ce un alt thread a apelat notify sau
notifyAll; poate a fost din cauza ca un alt predicat conditie asociat cu
aceeasi condition queue a devenit adevarat. Mai multe predicate conditie pe
conditie sunt destul de intalnite – BoundedBuffer foloseste aceeasi condition
queue pentru predicatele „not full” si „not empty”.
Pentru toate aceste motive cand trezesti din wake trebuie sa testezi
predicatul conditie din nou,si sa te intorci sau sa esuezi (fail) daca acesta nu
este inca adevarat. Cum poti sa te trezesti repetat,fara ca predicatul conditie
sa fie adevarat, trebuie aprope mereu sa apelezi wait dintr-un loop, testand
conditia predicat in fiecare iteratie. Forma canonica a unei conditii de
asteptare(condition wait) este prezentata in fig 14.7 .

Listing 14.7. Canonical form for state-dependent methods.

void stateDependentMethod() throws InterruptedException {


// condition predicate must be guarded by lock
synchronized(lock) {
while (!conditionPredicate())
lock.wait();
// object is now in desired state
}
}

Cand folosim asteptari conditionate (Object.wait sau Condition.await) :


- Intotdeauna sa avem un predicat conditie – unele testeaza starea
obiectului pe care acesta trebuie sa o aiba inainte sa continue
- Intotdeuna testati conditia predicat inainte sa apelati wait, si din nou
dupa ce v-ati intors din wait
- Intodeauna apelati wait intr-un loop
- Asigurati-va ca variabilele de stare care compun predicatul conditie
sunt pazite de un lacat asociat cu condition queue
- Tineti lacatul asociat cu condition queue cand apelati wait, notify si
notifyAll
- Nu deblocati lacatul dupa ce ati verificat conditia predicat(condition
predicate), ci inainte de a actiona pe aceasta

14.2.3 Semnale pierdute (Missed singnals)


Capitolul 10 discuta liveness failures precum deadlock si livelock. O alta
forma de liveness failure o reprezinta semnalele pierdute. Un semnal
pierdut se intampla cand un thread trebuie sa astepte pentru o conditie
specifica care deja este adevarata, dar esueaza in verificarea predicatului
conditie inainte sa astepte. Acum thread-ul asteapta sa fie notificat despre
un eveniment care deja s-a intamplat. Acest lucru este ca si cum am porni
toasterul, am merge sa luam ziarul, avand clopotelul sunat cat timp am fost
afara, si apoi sa stam in fata mesei de bucatarie asteptand sa sune
clopotelul. Poti sa astepti pe o perioada mare de timp, posibil pentru
totdeauna.Spre deosebire de marmelada pentru painea ta prajita, notficarea
nu este „lipicioasa” (sticky) – daca thread-ul A notifica pe o condition queue
si thread-ul B asteapta ulterior pe aceeasi condition queue, B nu se trezeste
imediat – o alta notificare este necesara pentru a trezi B.
Semnalele pierdute sunt rezultatul unor erori de codare, cum ar fi cele de
care am fost avertizati in lista de mai sus, cum ar fi esuarea testarii
predicatului conditie inainte de apelarea wait. Daca structurati conditia de
asteptare precum in 14.7, nu veti avea probleme cu semnale pierdute.

14.2.4 Notification
Pana acum, am descris jumatate din ce se intampla intr-o conditie de
asteptare(condition wait) : asteptarea . A doua jumatate este notificarea.
Intr-un buffer delimitat, take se blocheaza daca este apelat cand bufferul
este gol. Pentru ca take sa se deblocheze cand bufferul devine nevid,
trebuie sa ne asiguram ca fiecare cale de cod in care bufferul poate deveni
nevid trimite o notificare. In BoundedBuffer este doar un astfel de loc – dupa
un put. Deci put apeleaza notifyAll dupa ce a adaugat cu succes un obiect
la buffer. In mod similar, take apeleaza notifyAll dupa ce a scos un element
pentru a indica ca bufferul nu mai este plin, in caz ca orice thread asteapta
pe conditia „nu este plin”.
Mereu cand astepti pe o conditie, asigura-te ca cineva va face o notificare
cand predicatul conditie devine adevarat.

Exista 2 metode de notificare in API-ul condition queue – notify si notifyAll.


Pentru a apela oricare din acestea, trebuie sa tii lacatul asociat cu acel
obiect condition queque. Apelarea lui notify face ca JVM sa selecteze un
thread care asteapta pe acea coada de conditii(condition queue) sa se
trezeasca;apelarea lui notifyAll trezeste toate thread-urile care asteapta pe
acea condition queue. Deoarece trebuie sa tii lacatul pe obiectul condition
queue cand apelezi notify sau notifyAll, si threadurile care asteapta nu se
pot intoarce din wait fara sa reprimeasca lacatul, threadul notificator ar
trebui sa elibereze lacatul repede, pentru a se asigura ca thread-urile care
asteapta sunt deblocate cat mai devreme.
Deoarece mai multe threaduri pot sa astepte pe aceeasi condition
queue pentru predicate conditie diferite, folosirea lui notify in loc de notifyAll
poate fi periculoasa, in primul rand deoarece o singura notificare este
predispusa la o problema asemanatoare cu ratarea semnalelor.
BoundedBuffer ofera o buna ilustrare de ce notifyAll ar trebui folosit in
loc de notify in majoritatea cazurilor. Condition queue este folosita pentru 2
conditii predicat diferite : „not full” si „not empty”. Sa presupunem ca
threadul A asteapa pe o condition queue pentru predicatul PA, in timp ce
threadul B asteapta pe aceeasi condition queue pentru predicatul PB. Acum
sa presupunem ca predicatul PB devine adevarat si thread-ul C face o
singura notificare: JVM v-a trezi un thread la propria alegere. Daca A este
ales, se va trezi, v-a vedea ca PA nu este adevarat, si se v-a intoarce in
asteptare. Intre timp, B, care ar putea face progrese, nu se trezeste. Acesta
nu este neaparat un semnal pierdut, este mai mult un semnal furat – dar
problema este aceeasi : un thread asteapta pentru un semnal care a fost
(sau ar fi trebuit sa fie) aparut.

O notificare singura poate fi folosita in loc de notifyAll doar cand ambele


conditii urmatoare sunt indeplinite :
-Uniform waiters (care asteapta uniform). Doar un predicat conditie
este asociat cu condition queue, si fiecare thread executa aceeasi logica
asupra intoarcerii din wait.
- One-in,one-out. O notificare pe variabila de conditie permite cel mult
unui thread sa continue.
BoundedBuffer indeplineste cerinta „one-in, one out”, dar nu
indeplineste cerinta „uniform waiters” deoarece thread-urile care asteapta
(waiting threads) pot astepta fie dupa conditia „not full” fie dupa conditia „not
empty”. Un latch „starting gate” precum cel folosit in TestHarness la pagina
96, in care un singur eveniment elibereaza o multime de thread-uri, nu
indeplineste cerinta ”one-in, one-out”, deoarece deschiderea unui „starting
gate” permite mai multor thread-uri sa continue.
Majoritatea claselor nu indeplinesc aceste cerinte asa ca este mai
intelept sa folosim notifyAll decat notify simplu. Cat timp acest lucru poate fi
ineficient, este mult mai usor sa ne asiguram ca se comporta corect clasele
noastre cand folosim notifyAll in loc de notify.
Acest „mai intelept” este incomfortabil pentru unii oameni, si pentru buna
dreptate. Folosirea lui notifyAll cand doar un thread poate progresa este
ineficienta – uneori putin, alteori foarte mult. Daca 10 thread-uri asteapta pe
o coada de conditii(condition queue), apelarea lui notifyAll face ca fiecare
din ele sa se trezeasca si sa ceara lacatul, majoritatea se vor intoarce in
starea „sleep”. Aceasta inseamna o muntime de switch-uri de context, si o
multime de achizii de blocari in lupta(contented lock aquisitions) pentru
fiecare eveniment care permite (poate) unui singur thread sa faca progrese.
(In cel mai rau caz, folosirea lui notifyAll ar rezulta in O(n2) treziri unde n ar
fi suficient). Aceasta este o alta situatie unde preocuparile legate de
performanta sustin o abordare si precuparile legate de siguranta sustin o
alta abordare.
Notificarea facuta de put si take in BoundedBuffer este conservativa : o
notificare este efectuata de fiecare data cand un obiect este pus sau
eliminat din buffer. Aceasta poate fi optimizata observand ca un thread
poate fi eliberat dintr-un wait doar daca bufferul trece dintr-o stare „empty” in
„non-empty” sau din „full” in „not full”, si notificand doar daca un put sau un
take a efectuat una din aceste tranzitii de stare.Aceasta este denumita
„notificare coditionala”. Cat timp notificarea conditionala poate creste
performanta, este dificil de a obtine dreptul(si de asemenea complica
implementarea subclaselor), si ar trebui folosita cu grija.
Notificarea simpla si notificarea conditionala sunt optimizari. Ca
intotdeauna, trebuie sa urmam principiul „Intai sa faci un lucru, apoi sa il faci
rapid – doar daca nu este deja destul de rapid” cand folosim astfel de
optimizari; este usor sa introducem liveness failures ciudate aplicandu-le
incorect.

Listing 14.8. Using conditional notification in BoundedBuffer.put.


public synchronized void put(V v) throws InterruptedException {
while (isFull())
wait();
boolean wasEmpty = isEmpty();
doPut(v);
if (wasEmpty)
notifyAll();
}

14.2.5 O clasa gate(poarta)


Latch-ul starting gate din TestHarness de la pagina 96 a fost construit cu un
contor initial de 1, creeand un latch binar : unul cu 2 stari, starea intiala si
starea finala. Latch-ul previne thread-urile sa treaca mai departe prin cand
starting gate pana cand nu este deschis, moment in care toate thread-urile
pot sa progreseze. In timp ce acest mecanism de blocare este de multe ori
exact de ceea ce avem nevoie, uneori exista un dezavantaj ca o poarta
construita in acest mod nu potate fi reinchisa odata ce a fost deschisa.
Este usor sa developam o clasa ThreadGate care sa poate fi inchisa
folosind asteptari conditionate(condition waits), asa cum vom arata in fig
14.9 . Clasa ThreadGate permite portii(gate) sa fie deschisa si inchisa,
oferind o metoda await care se blocheaza pana cand poarta este deschisa.
Metoda open foloseste notifyAll deoarece semnatica acestei clase esueaza
testul „one-in,one out” pentru notificari singulare.
Predicatul conditie folosit de await este mai complicat decat simpla
testarea a lui isOpen(). Acesta este necesar deoarece daca n thread-uri
asteapta pentru poarta(gate) la un momentdat, ar trebui sa aiba toate
permis sa continue. Dar, daca poarta este deshisa si inchisa in succesiuni
rapide, toate thread-urile pot sa nu fie eliberate daca await verifica doar
isOpen : pana cand toate thead-urile primesc notificare, reprimesc lacatul si
ies din wait , poarta poate fi inchisa din nou. Asa ca ThreadGate foloseste o
conditie predicat cumva mai complicata : de fiecare data cand o
poarta(gate) este inchisa, un counter ”generation” este incrementat, si un
thread poate trece de await daca poarta este deschisa acum sau daca
poarta a fost deschisa de cand thread-ul a ajuns la poarta.
Cum ThreadGate suporta doar asteptarea ca poarta sa fie deschisa, el
face notificari doar in open ; pentru a suporta atat operatia „wait for open”
cat si „wait for close”, ar trebui sa notifice in ambele open si close. Aceasta
ilustreaza de ce clasele dependente de stare sunt greu de
intretinut(maintain) – adaugarea unei noi operatii dependete de stare poate
necesita modificarea multor cai de cod care modifica starea obiectului astfel
incat notificarile adecvate sa poata fi efectuate.

14.2.6 Probleme legate de siguranta subclaselor


Folosirea notificarii conditionale sau unice introduce constrangeri care pot
complica creearea subclaselor. Daca vrei sa ai suport pentru subclase,
trebuie sa-ti structurezi clasa astfel incat subclasele sa poate adauga
notificarea in numele clasei parinte daca subclasa este creeata intr-un mod
care incalca una din cerintele notificarii unice sau conditionale.
O clasa dependenta de stare ar trebui fie sa expuna si sa documenteze
protocoalele de waiting si notification pentru subclase, fie sa previna
subclasele de la a participa in ele. Cel putin, proiectearea claselor depente
de stare pentru mostenire necesita expunerea cozilor de conditii (condition
queues) si a lacatelor si documentarea conditiilor predicat si a politicii de
sincronizare; poate de asemenea fi necesara variabilelor de stare
importante. (Cel mai rau lucru pe care il poti face cu o clasa dependenta de
stare este sa expui starea subclaselor, dar sa nu documentezi protocoalele
de asteptare(waiting) si notificare(notification). Aceasta este precum o clasa
care isi expune variabilele de stare dar nu-si documenteaza invariantii).
O optiune pentru a face acest lucru este sa interzici efectiv creearea
de subclase, fie marcand clasa ca final, fie ascunzand cozile de
conditii(condition queues), lacatele si variabilele de stare de subclase.
Altfel, daca subclasa face ceva sa submineze modul in care clasa de baza
foloseste notify, aceasta trebuie sa fie capabila sa repare daunele(damage).
Sa consideram o stiva de blocare nedelimitata in care operatia pop se
blocheaza daca stiva este goala dar operatia push poate intodeauna sa se
efectueze. Aceasta indeplineste cerintele pentru notificare simpla(unica).
Daca aceasta clasa foloseste notificare simpla si o subclasa adauga o
metoda „pop two consecutive elements”, acum sunt 2 tipuri de asteptari:
acele asteptari sa scoata un element din stiva (pop one element) si acelea
pentru a scoate al doilea element. Dar in cazul in care clasa de baza
expune coada conditie (condition queue) si documenteaza protocoalele sale
de folosire, subclasa poate suprascrie metoda push sa efectueze un
notifyAll, restabilind siguranta.

14.2.8 Protocoalele entry si exit


Wellings caracteriza folosirea corecta a lui wait si notify in termeni de
entry protocol si exit protocol. Pentru fiecare operatie dependenta de stare
si pentru fiecare operatie care modifica starea pe care alta operatie are
dependenta de stare, ar trebui sa definim si sa documentam un entry
protocol si un exit protocol. Protocolul entry este conditia predicat a
operatiei; protocolul exit implica examinarea oricarei variabile de stare care
a fost schimbata de operatie sa vedem daca a cauzat ca alta conditie
predicat sa devina adevarata, si daca da, sa notifice pe coada de conditie
asociata.
AbstractQuequedSynchronizer, pe care majoritatea claselor
dependente de stare din java.util.concurrent sunt construite(vezi sectiunea
14.4) exploateaza conceptul de protocol exit (de iesire). Mai degraba decat
sa permitem claselor syncronizate sa faca propria notificare, este in schimb
nevoie ca metodele sincronizate sa intoarca o valoare care indica daca
actiunea lor ar putea debloca unul sau mai multe thread-uri in asteptare.
Aceasta cerinta explicita de API face mai greu sa uitam sa notificam pe
unele tranzitii de stare.

14.3 Obiecte conditii explicite


Asa cum am vazut in capitolul 13, lacatele explicite (explicit Locks) pot fi
folositoare in situatiile in care lacatele intrinseci sunt prea inflexibile. Precum
Lock este o generalizare a lacatelor intrinseci, Condition este o generalizare
a cozilor de conditii intrinseci (intrinsic condition queues).
Cozile de conditii intrinseci au cateva dezavantaje. Fiecare lacat
intrinsec poate avea doar o coda de conditii(condition queue) asociata, ceea
ce inseamna ca in clase ca BoundedBuffer, thread-uri multiple pot astepta
pe aceeasi coada conditie pentru diferite conditii predicat, si cel mai folosit
pattern pentru blocare implica expunerea obiectelor cozi conditie. Ambii
factori fac imposibila aplicarea obligatiei „uniform waiter” pentru folosirea
notificarii. Daca vrei sa scrii un obiect concurent cu mai multe conditii
predicat, sau vrei sa ai mai mult control asupra vizibilitatii cozilor de conditii,
clasele explicite Lock si Condition ofera o alternativa mai flexibila la lacatele
intrinseci(intrinsic locks) si la cozile de conditii (condition queues).
O Condition este asociata cu un singur Lock, asa cum o coada de
conditii este asociata cu un singur lacat intrinsec (single intrinsic lock).
Pentru a creea o Condition, apelam Lock.newCondition pe lacatul asociat.
Si precum Lock ofera o proprietati mai bogate decat blocarea intrinseca,
Condition ofera o proprietati mai bogate decat cozile intrinseci de conditii
(intrinsic condition queues) : mai multe asteptari pe lacat (multiple wait sets
per lock), asteptari conditionate intreruptibile si neintreruptibile, asteptare
bazata pe deadline, si o optiune de asteptare(stat la coada) fair sau nonfair.

Listing 14.10. Condition interface.

public interface Condition {


void await() throws InterruptedException;
boolean await(long time, TimeUnit unit)
throws InterruptedException;
long awaitNanos(long nanosTimeout) throws InterruptedException;
void awaitUninterruptibly();
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();
}

Spre deosebire de cozile de conditii intrinseci, poti avea cat de multe


obiecte Condition pe Lock. Obiectele Condition mostenesc setarile de
fairness ale obiectului Lock asociat; pentru fair locks (lacate corecte),
thread-urile sunt eliberate din Condition.await in ordinea FIFO.

Avertizare de hazard : Echivalentele lui wait, notify si notifyAll pentru


obiecte conditie(condition objects) sunt await, signal si signalAll. Totusi,
Condition extinde Object, ceea ce inseamna ca inca are metodele wait si
notify. Fiti siguri ca folositi versiunile adecvate – await si signal in acest caz.

Figura 14.11 ne arata o alta implementare a bufferului delimitat, de


aceasta data folosind doua Conditions notFull si notEmpty, pentru a
reprezenta explicit conditiile predicat „not full” si „not empty”. Cand take se
blocheaza deoarece bufferul este gol,asteapta pe notEmpty, si put
deblocheaza orice thread blocat in take prin semnalizarea(signaling) pe
notEmpty.
Comportamentul lui ConditionBoundedBuffer este acelasi ca la
BoundedBuffer, dar folosirea cozilor de conditii este mai usor de citit, este
mai usor sa analizam o clasa care foloseste mai multe obiecte Conditions
decat una care foloseste aceeasi coada de conditii intrinseca cu mai multe
conditii predicat(condition predicates). Prin separarea acestor 2 predicate
conditii in in multimi de astepare separate, Condition face mai usor sa
indeplinim cerintele pentru notificarea singulara. Folosirea mult mai
eficientului signal in loc de signalAll reduce numarul de switch-uri de context
(context switches) si de achizitii de blocari declansate de fiecare operatie pe
buffer.
La fel cu lacatele intrinseci si cozile de conditii, relatia 3-cai intre lock,
prodicatul conditie si variabilele conditie trebuie de asemenea sa aiba loc
atunci cand folosim obiecte explicite Lock si Condition. Variabilele implicate
in predicatul conditie trebuie sa fie pazite de lacat(Lock) si lacatul trebuie sa
fie tinut cand testam predicatul conditie si cand apelam await si signal.

Alegeti dintre folosirea explicita a obiecului Condition si coada de


conditii intrinseca in acelasi mod in care trebuie sa alegem intre
ReentrantLock si synchronized; folositi Condition daca aveti nevoie de
proprietati avansate cum sunt fair queueing si multimi de asteptare separate
pe blocare/lacat, altfel sunt de preferat cozile de conditii intrinseci(intrinsic
condition queue).(Daca deja folosesti ReentrantLock pentru ca iti trebuie
proprietatile avansate, alegerea este deja facuta)
public class ConditionBoundedBuffer<T> {
protected final Lock lock = new ReentrantLock();
// CONDITION PREDICATE: notFull (count < items.length)
private final Condition notFull = lock.newCondition();
// CONDITION PREDICATE: notEmpty (count > 0)
private final Condition notEmpty = lock.newCondition();
@GuardedBy("lock")
private final T[] items = (T[]) new Object[BUFFER_SIZE];
@GuardedBy("lock") private int tail, head, count;
// BLOCKS-UNTIL: notFull
public void put(T x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[tail] = x;
if (++tail == items.length)
tail = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
// BLOCKS-UNTIL: notEmpty
public T take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
T x = items[head];
items[head] = null;
if (++head == items.length)
head = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}

14.4 Anatomia unui Synchronizer


Interfetele ReentrantLock si Semaphore au o multime de lucruri in
comun. Ambele clase se comporta ca „porti” („gate”), permitand numai unui
numar mic de thread-uri sa treaca la un moment dat; thread-urile ajung la
„poarta” si le este permis sa treaca (lacatul este eliberat cu succes) sau sunt
puse in asteptare(lacatul este blocat) sau sunt intoarse (tryLock or
tryAcquire intoarce fals, idicand ca lacatul nu a devenit disponibil in timpul
permis).
Mai mult, ambele permit incercari de achizitii interuptibile,neinteruptibile si
cronometrate/temporizate,si ambele permit alegerea unei asteptari a thread-
urilor fair sau nonfair(corecte sau noncorecte).
Avand in vedere aceste generalitati in comun,am putea crede ca
Semaphore a fost implementat pe baza ReentrantLock, sau poate
ReentrantLock a fost implementat ca un Semaphore cu un permis (with one
permit). Aceasta ar fi practic in totalitate, este un exercitiu comun sa
dovedim ca un semafor de numarare poate fi implementat folosind un lacat
(precum in SemaphoreOnLock in 14.12) si ca un lacat poate fi implementat
folosind un semafor de numarare.
De fapt, ambele sunt implementate cu ajutorul unei clase de baza
comune, AbstractQueuedSychronizer(AQS) – cum sunt multe alte
sychronizers. AQS este un framework pentru a construi lacate si
sincronizatoare, si o gama surprinzator de larga de sychronizers poate fi
construita usor si eficient folosind-o. Nu numai ReentrantLock si Semaphore
au fost construite folosind AQS, dar la fel au fost construite
CountDownLatch, ReentrantReadWriteLock,SynchronousQueue si
FutureTask.
Listing 14.12. Counting semaphore implemented using Lock.

@ThreadSafe
public class SemaphoreOnLock {
private final Lock lock = new ReentrantLock();
//CONDITION PREDICATE: permitsAvailable (permits > 0)
private final Condition permitsAvailable = lock.newCondition();
@GuardedBy("lock") private int permits;
SemaphoreOnLock(int initialPermits) {
lock.lock();
try {
permits = initialPermits;
} finally {
lock.unlock();
}
}
//BLOCKS-UNTIL: permitsAvailable
public void acquire() throws InterruptedException {
lock.lock();
try {
while (permits <= 0)
permitsAvailable.await();
--permits;
} finally {
lock.unlock();
}
}
public void release() {
lock.lock();
try {
++permits;
permitsAvailable.signal();
} finally {
lock.unlock();
}
}
}

AQS se ocupa de multe din detaliile implementarii unui synchronizer,


cum sunt queuing FIFO sau thread-urile in asteptare. Synchronizer-i
individuali pot defini un criteriu flexibil pentru cazul in care un thread ar
trebui sa aiba permisa trecerea sau ar trebui sa astepte.
Folosirea AQS pentru a construi synchronizers ofera cateva beneficii.
Nu doar reduce substantial efortul de implementare, dar de asemenea nu
trebuie sa plateasca pentru mai multe puncte de disputa, asa cum ar fi cand
construim un synchronizer pe baza altuia. In SemaphoreOnLock, obtinerea
unui permit are doua locuri in care s-ar putea bloca – odata la lacatul care
pazeste starea semaforului, si apoi iarasi in cazul in care un permit nu este
disponibil. Sincronizatorii construiti cu AQS au doar un punct unde se pot
bloca, reducand problemele legate de context-switch si imbunatatind
tranzitia. AQS a fost proiectat pentru scalabilitate, si toti
sincronizatorii(synchronizers) in java.util.concurrent sunt construiti cu
beneficiile AQS din acesta.

14.5 AbstractQueuedSynchronizer
Cei mai multi developeri probabil nu vor folosi niciodata AQS in mod
direct; Setul standard de sinchronizers acopera o gama destul de larga de
situatii.
Dar sa vedem cum sincronizatorii standard sunt implementati ne poate ajuta
sa clarificam modul in care functioneaza.

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