Documente Academic
Documente Profesional
Documente Cultură
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 {
@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);
}
}
@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.)
@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);
}
}
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
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
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; }
}
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.
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 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.
@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);
}
}
//Unsafe publication
public Holder holder;
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.
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.
@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);
}
}
}
@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;
}
}
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.
@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();
}
}
}
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.