Klasifikacija brava i pregled brava u JUC paketu — sve na jednom mestu
Ranije smo predstavili ugrađenu bravu zasnovanu na objektima — synchronized. Zapravo, Java u paketu java.util.concurrent (JUC) nudi dodatne klase i interfejse brava (naročito u podpaketu locks), sa moćnijim mogućnostima ili boljim performansama.
Hajde da pogledamo nedostatke synchronized.
- Ako je kritični deo samo za čitanje, više niti bi moglo da ga izvršava istovremeno, ali sa synchronized u jednom trenutku može da izvršava samo jedna nit.
- synchronized ne može da sazna da li je nit uspešno dobila bravu.
- Uz synchronized, ako se kritični deo blokira zbog IO-a ili metode sleep, a trenutna nit nije pustila bravu, dovest će do toga da sve niti čekaju.
Kritični deo (Critical Section) je važan koncept u višenitnom radu; označava onaj deo koda koji pristupa deljenim resursima i u kojem u svakom trenutku može biti samo jedna nit. Ako više niti istovremeno pristupi resursima kritičnog dela bez ikakve sinhronizacije (brave), stanje resursa postaje nepredvidivo i nekonzistentno, što dovodi do takozvanog „trkačkog stanja" (Race Condition). U mnogim strategijama istovremenog upravljanja, npr. uz mutex brava synchronized, cilj je da se osigura da u svakom trenutku samo jedna nit uđe u kritični deo.
Međutim, ovi nedostaci synchronized se mogu nadoknaditi drugim bravama iz JUC paketa; hajde prvo da pogledamo klasifikaciju brava.
Nekoliko kategorija brava
Java nudi bogat izbor brava; svaka od njih, zahvaljujući svojim specifičnostima, u odgovarajućim scenarijima može biti veoma efikasna. Možemo ih grupisati prema karakteristikama.

Optimistička VS pesimistička brava
Optimistička i pesimistička brava su šire, opšte pojmove koje izražavaju različite uglove gledanja na sinhronizaciju niti.
Prvo, pojmovi. Pri istovremenom radu nad istim podacima, pesimistička brava smatra da će, dok ona koristi podatke, druga nit sigurno promeniti te podatke, pa pri preuzimanju podataka prvo stavlja bravu kako bi osigurala da druga nit ne menja podatke. U Javi je ključna reč synchronized tipična pesimistička brava.
Optimistička brava, s druge strane, smatra da dok ona koristi podatke neće ih druga nit menjati, pa ne stavlja bravu; prilikom ažuriranja podataka samo proverava da li ih je neka druga nit u međuvremenu promenila. Ako podaci nisu promenjeni, trenutna nit upisuje svoje izmene. Ako su podatke već promenila neka druga nit, postupa se različito, zavisno od implementacije (npr. greškom ili automatskim ponavljanjem).
U Javi se optimistička brava ostvaruje programiranjem bez brava, najčešće uz CAS algoritam; operacija uvećanja Java atomskih klasa realizovana je CAS-spinom.

Na osnovu opisa pojmovalmožemo primetiti:
- Pesimistička brava odgovara scenarijima sa mnogo operacija pisanja — prethodno zaključavanje osigurava ispravnost podataka pri pisanju.
- Optimistička brava odgovara scenarijima sa mnogo operacija čitanja — odsustvo brave drastično poboljšava performanse čitanja.
Samo pojmovi su pomalo apstraktni; pogledajmo načine poziva optimističke i pesimističke brave:
// --------- Način poziva pesimističke brave -------------------------
// synchronized
public synchronized void testMethod() {
// rad sa sinhronizovanim resursom
}
// ReentrantLock
private ReentrantLock lock = new ReentrantLock();
// potrebno je osigurati da više niti koristi istu bravu
public void modifyPublicResources() {
lock.lock();
// rad sa sinhronizovanim resursom
lock.unlock();
}
// --------- Način poziva optimističke brave -------------------------
private AtomicInteger atomicInteger = new AtomicInteger();
// potrebno je osigurati da više niti koristi isti AtomicInteger
atomicInteger.incrementAndGet(); // izvršava uvećanje za 1Kroz primere načina poziva vidimo da se pesimistička brava uglavnom koristi tek nakon eksplicitnog zaključavanja, dok optimistička brava direktno pristupa sinhronizovanim resursima. Zašto optimistička brava može ispravno da ostvari sinhronizaciju niti a da ne zaključava resurse? Podsetimo se ponovo tehničkog principa „CAS"; već smo o tome govorili, pa neka ovo bude ponavljanje.
CAS je algoritam bez brava — može da ostvari sinhronizaciju promenljivih između niti bez korišćenja brava (nema blokiranih niti). Atomske klase u JUC paketu (o kojima će kasnije biti više reči — kliknite na link) upravo su optimističke brave realizovane preko CAS-a.
CAS algoritam uključuje tri operanda:
- memorijsku vrednost koju treba pročitati/upisati V.
- vrednost za poređenje A.
- novu vrednost koja se upisuje B.
Samo ako je V jednako A, CAS atomično ažurira V na novu vrednost B („uporedi + ažuriraj" je celina atomska operacija); inače ne preduzima ništa. Uglavnom je „ažuriranje" operacija koja se stalno ponavlja.
Ranije smo pomenuli atomske klase u JUC paketu kao optimističke brave realizovane CAS-om; zagradimo se u izvorni kod atomske klase AtomicInteger (i o njoj će kasnije biti detaljno reči, ali pošto smo već tu, prodajmo to). Pogledajmo definiciju AtomicInteger:

Iz definicije vidimo ulogu pojedinih svojstava:
- unsafe: dohvata i opslužuje podatke u memoriji.
- valueOffset: čuva ofset vrednosti
valueu okviru AtomicInteger-a. - value: čuva int vrednost AtomicInteger-a; ovo svojstvo mora uz pomoć ključne reči volatile da bude vidljivo među nitima.
Zatim, pregledamo metodu uvećanja incrementAndGet() AtomicInteger-a i vidimo da ona u osnovi poziva unsafe.getAndAddInt(). Ali pošto JDK sam ima samo Unsafe.class, imena argumenata u class datoteci ne otkrivaju dobro svrhu metode, pa preko OpenJDK 8 pogledajmo izvorni kod klase Unsafe (i o njemu će biti reči — kliknite na link):
// ------------------------- JDK 8 -------------------------
// Metoda uvećanja AtomicInteger-a
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// Unsafe.class
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
// ------------------------- OpenJDK 8 -------------------------
// Unsafe.java
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}Iz izvornog koda vidimo da getAndAddInt() u petlji dohvata vrednost v na zadatom ofsetu objekta o, a zatim proverava da li je memorijska vrednost jednaka v. Ako jeste, postavlja memorijsku vrednost na v + delta; inače vraća false i nastavlja petlju sve dok ne uspe, a onda vraća staru vrednost.
Celokupna operacija „uporedi + ažuriraj" sadržana je u compareAndSwapInt(). U JNI-ju se obavlja uz pomoć jedne CPU instrukcije i atomska je, pa može da garantuje da više niti vidi istu izmenu vrednosti iste promenljive.
Java Native Interface (JNI) je most između Jave i nativnog koda (npr. C, C++). Omogućava Java kodu da komunicira sa nativnim API-jima i lokalnim bibliotekama i da obavi neke zadatke koje Java ne može lako sama.
Kasniji JDK koristi CPU instrukciju cmpxchg da uporedi A u registru sa vrednošću V u memoriji. Ako su jednaki, u memoriju se upisuje nova vrednost B. Ako nisu, memorijska vrednost V se upisuje u registar na mesto A. Zatim while petlja u Java kodu ponovo poziva cmpxchg instrukciju i ponavlja dok ne uspe.
CMPXCHG je skraćenica od „Compare and Exchange"; to je atomska instrukcija za bezbedno menjanje deljenih podataka u višejezgarnoj/višenitnoj sredini. CMPXCHG postoji u mnogim savremenim mikroprocesorskim arhitekturama, npr. Intel x86/x64. Za 32-bitne operande instrukcija se obično zove CMPXCHG, a za 64-bitne operande CMPXCHG8B ili CMPXCHG16B.
Iako je CAS efikasan, ima tri velika problema, o kojima smo već govorili. Nadam se da ih se još sećate; ako ne, kliknite na link i podsetite se.
Spin-brava VS adaptivna spin-brava
Blokirati ili probuditi Java nit zahteva od operativnog sistema da promeni stanje CPU-a, a ta promena stanja košta procesorsko vreme. Ako je sadržaj sinhronizovanog bloka previše jednostavan, vreme promene stanja može biti duže od vremena izvršenja korisničkog koda.
U mnogim scenarijima vreme zaključanosti sinhronizovanog resursa je kratko; radi tog kratkog intervala menjati niti, trošiti vreme na obustavljanje i ponovno pokretanje niti, može se ispostaviti kao neto gubitak za sistem. Ako fizička mašina ima više procesora i omogućava da dve ili više niti paralelno izvršavaju, možemo dozvoliti niti koja kasnije traži bravu da ne odustaje od CPU vremena, već da sačeka i vidi da li će nit koja drži bravu brzo da je pusti.
Da bi trenutna nit „malo sačekala", puštamo je da spinuje. Ako nakon spinovanja nit koja je zaključala resurs već pusti bravu, trenutna nit može da preuzme resurs bez blokiranja, čime se izbegava trošak promene niti. To je spin-brava.

Spin-brava ima i svoje mane; ne može da zameni blokiranje. Iako spin-čekanje izbegava trošak promene niti, ono zauzima procesorsko vreme. Ako je brava kratko zauzeta, spin-čekanje je vrlo efikasno. Ako je brava dugo zauzeta, spinujuća nit samo uzalud troši procesorske resurse. Zato spin-čekanje mora imati neku granicu — ako broj spinova pređe dozvoljeni broj (podrazumevano 10, može se promeniti uz -XX:PreBlockSpin) a nit ne dobije bravu, treba je obustaviti.
Princip realizacije spin-brave takođe je CAS; do-while petlja u izvornom kodu AtomicInteger-a koja preko unsafe-a obavlja uvećanje jeste spin operacija — ako izmena vrednosti ne uspe, petljom se ponavlja spin, sve dok ne uspe.

Spin-brava je uvedena u JDK 1.4.2, uz -XX:+UseSpinning za uključivanje. U JDK 6 postala je podrazumevano uključena i uvedena je i adaptivna spin-brava.
Adaptivnost znači da vreme (broj) spinovanja više nije fiksno, već zavisi od prethodnog vremena spinovanja na istoj bravi i stanja vlasnika brave. Ako je na istom objektu brave spinovanjem nedavno već uspešno dobijena brava, a nit koja drži bravu radi, virtuelna mašina će proceniti da će i ovaj spin verovatno uspeti i dozvoliti duže spinovanje. Ako se za neku bravu spinovanjem retko uspeva, pri narednim pokušajima dobijanja te brave proces spinovanja se može preskočiti i nit odmah blokirati, izbegavajući rasipanje procesorskih resursa.
Brava bez brave, biased brava, laka brava, teška brava
Ove četiri vrste brava odnose se isključivo na synchronized; detaljno smo ih opisali u tekstu šta tačno zaključava synchronized, pa ih ovde nećemo ponavljati.
Reentrantna i nereentrantna brava
Reentrantna brava je poznata i kao rekurzivna brava: kada ista nit dobije bravu u spoljašnjoj metodi, pri ulasku u unutrašnju metodu te niti automatski dobija bravu (uslov: zaključan je isti objekat ili klasa), bez blokiranja zato što je ranije dobijena a još nije puštena. U Javi su i ReentrantLock (o kom će kasnije biti reči — kliknite na link) i synchronized reentrantne brave; jedna od prednosti reentrantne brave je da do izvesnog stepena izbegava mrtvu blokadu. Analiza primerom:
public class Widget {
public synchronized void doSomething() {
System.out.println("Metod 1 izvršen...");
doOthers();
}
public synchronized void doOthers() {
System.out.println("Metod 2 izvršen...");
}
}U gornjem kodu su obe metode klase dekorisane ugrađenom bravom synchronized, a doSomething() poziva doOthers(). Pošto je ugrađena brava reentrantna, ista nit pri pozivu doOthers() može direktno da dobije bravu trenutnog objekta i uđe u doOthers().
Kod nereentrantne brave, trenutna nit pre poziva doOthers() mora da pusti bravu koju je dobila u doSomething() — a tu bravu već drži i ne može da je pusti. Tada nastaje mrtva blokada.
Zašto onda reentrantna brava može automatski da dobije bravu pri ugnježdenom pozivu?
Vratimo se primeru s točenjem vode: više ljudi čeka u redu; administrator dozvoljava da se brava veže za više kofa iste osobe. Ta osoba, kad toči vodu u više kofa, nakon što prva kofa veže bravu i napuni se, i druga kofa može odmah da veže bravu i počne točenje; tek kad se sve kobe napune, čovek vraća bravu administratoru. Ceo proces za tu osobu uspešno se odvija, a i ljudi u redu kasnije mogu da toče vodu. To je reentrantna brava.

A kod nereentrantne brave administrator dozvoljava da se brava veže samo za jednu kofu te osobe. Prva kofa se veže za bravu i napuni, ali se brava ne pušta, pa druga kofa ne može da se veže niti da toči vodu. Trenutna nit zapada u mrtvu blokadu i sve niti u redu čekanja ne mogu da se probude.

Ranije smo rekli da su i ReentrantLock i synchronized reentrantne brave; uporedimo preko izvornog koda reentrantnu bravu ReentrantLock i nereentrantnu bravu NonReentrantLock i analizirajmo zašto nereentrantna brava pri ponovljenom pozivu sinhronizovanog resursa zapada u mrtvu blokadu.
I ReentrantLock i NonReentrantLock nasleđuju roditeljsku klasu AQS, čija se sinhrona stanje status koristi za brojanje broja reentrantnosti; početna vrednost status je 0.
Kad nit pokuša da dobije bravu, reentrantna brava prvo pokušava da dobije i ažurira status; ako je status == 0, to znači da druga nit ne izvršava sinhronizovani kod, pa se status postavlja na 1 i trenutna nit počinje izvršenje. Ako je status != 0, proverava da li je trenutna nit ta koja je dobila bravu; ako jeste, izvršava status+1 i trenutna nit može ponovo da dobije bravu.
Nereentrantna brava direktno dobija i pokušava da ažurira trenutnu vrednost status; ako je status != 0, to dovodi do neuspeha i trenutna nit se blokira.
Pri puštanju brave, reentrantna brava prvo dohvata trenutni status (pod uslovom da je trenutna nit vlasnik brave). Ako je status-1 == 0, to znači da su sve operacije ponovnog dobijanja brave završene i nit tada stvarno pušta bravu. Nereentrantna brava, pošto utvrdi da je trenutna nit vlasnik brave, odmah postavlja status na 0 i pušta bravu.

Pravedna i nepravedna brava
Ovde „pravedno" zapravo znači „prvi došao prvi uslužen", odnosno FIFO. Ako se za bravu zahtev niti koje prvo zatraže bravu uvek prvo zadovolje, a zahtevi niti koje kasnije zatraže zadovolje kasnije, ta brava je pravedna. U suprotnom, nije pravedna.
Uobičajeno, nepravedna brava može donekle da poboljša efikasnost, ali može dovesti do izgladnjivanja niti (neke niti dugo ne mogu da dobiju bravu). Zato treba birati između nepravedne i pravedne brave prema stvarnim potrebama.
ReentrantLock podržava i nepravednu i pravednu varijantu.
Brava za čitanje/pisanje i ekskluzivna brava
synchronized, o kome smo već govorili, i ReentrantLock, o kome će tek biti reči, zapravo su „ekskluzivne brave". Drugim rečima, te brave u istom trenutku dozvoljavaju pristup samo jednoj niti.
Brava za čitanje/pisanje u istom trenutku može dozvoliti pristup više čitalačkih niti. Java nudi klasu ReentrantReadWriteLock (o kojoj će kasnije biti detaljno reči — kliknite na link) kao podrazumevanu implementaciju brave za čitanje/pisanje; interno održava dve brave: jednu za čitanje i jednu za pisanje. Razdvajanjem brave za čitanje i brave za pisanje, u sredinama gde se „mnogo čita, malo piše", performanse se znatno poboljšavaju.
Pazite, čak i uz bravu za čitanje/pisanje, kad pristupa nit koja piše, sve niti koje čitaju i ostale niti koje pišu se blokiraju.
Ekskluzivna brava se zove i isključiva brava: ako nit T stavi ekskluzivnu bravu na podatak A, druge niti ne mogu staviti nikakvu vrstu brave na A. Nit koja dobije ekskluzivnu bravu može i da čita i da menja podatak.
Suprotno tome, deljiva brava je ona koju može držati više niti. Ako nit T stavi deljivu bravu na podatak A, druge niti na A mogu dodati samo deljivu bravu, ne i ekskluzivnu. Nit koja drži deljivu bravu može samo da čita podatak, ne i da ga menja.
Isključiva i deljiva brava takođe se ostvaruju preko AQS — implementacijom različitih metoda.
Ispod je deo izvornog koda ReentrantReadWriteLock:

Vidimo da ReentrantReadWriteLock ima dve brave: ReadLock i WriteLock; iz imena je jasno — jedna za čitanje, jedna za pisanje, zajedno „brava za čitanje/pisanje". Daljom analizom vidimo da su ReadLock i WriteLock realizovani preko interne klase Sync. Sync je podklasa AQS-a; ista struktura postoji i u CountDownLatch, Semaphore (o kojima će kasnije biti reči — kliknite na link) i ReentrantLock (o kom će sledećem biti reči — kliknite na link).
U ReentrantReadWriteLock glavno telo i brave za čitanje i za pisanje je Sync, ali se način zaključavanja razlikuje. Brava za čitanje je deljiva, brava za pisanje je isključiva. Deljiva brava čitanja omogućava vrlo efikasno paralelno čitanje, dok su procesi čitanje-pisanje, pisanje-čitanje i pisanje-pisanje međusobno isključivi, jer su brava čitanja i brava pisanja razdvojene. Zato je istovremenost ReentrantReadWriteLock-a znatno bolja od uobičajene mutex brave.
Koja je konkretna razlika u načinu zaključavanja brave za čitanje i brave za pisanje?
Pre nego što zagledamo izvorni kod, podsetimo se još nečega. Na početku, kad smo spominjali AQS, pomenuli smo i polje state (int, 32 bita) koje opisuje koliko niti drži bravu.
U isključivoj bravi ta vrednost je obično 0 ili 1 (kod reentrantne brave vrednost state je broj reentrantnosti); u deljivoj bravi state je broj niti koje drže bravu. Ali u ReentrantReadWriteLock postoje dve brave — čitanje i pisanje — pa u jednom celobrojnom polju state treba opisati i broj (ili stanje) brave čitanja i brave pisanja.
Zato se promenljiva state „iseče po bitovima" na dva dela: gornjih 16 bita opisuju stanje brave čitanja (broj čitanja), donjih 16 bita stanje brave pisanja (broj pisanja). Kao na slici:

Pošto smo razumeli pojam, pogledajmo kod; prvo kod zaključavanja brave za pisanje:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState(); // dohvata trenutni broj brava
int w = exclusiveCount(c); // dohvata broj brava pisanja w
if (c != 0) { // ako neka nit već drži bravu (c!=0)
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread()) // ako je broj niti koje pišu (w) 0 (dakle postoji brava čitanja) ili nit koja drži bravu nije trenutna nit, vraća neuspeh
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT) // ako broj brava pisanja prelazi maksimum (65535, 2^16-1), baca Error
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) // ako je broj niti koje pišu 0 i trenutna nit treba da se blokira, vraća neuspeh; ili ako CAS-om ne uspe povećanje broja niti koje pišu, takođe vraća neuspeh
return false;
setExclusiveOwnerThread(current); // ako je c=0, w=0 ili c>0, w>0 (reentrantnost), postavlja trenutnu nit kao vlasnika brave
return true;
}- Ovaj kod prvo dohvata trenutni broj brava c, a zatim iz c dobija broj brava pisanja w. Pošto je brava pisanja u donjih 16 bitova, uzima se maksimum donjih 16 bitova i radi se AND sa trenutnim c (
int w = exclusiveCount©;); gornjih 16 bitova AND sa 0 daje 0, ostaje vrednost donjih bitova, što je istovremeno i broj niti koje drže bravu pisanja. - Nakon dohvata broja niti koje pišu, prvo se proverava da li bravu već drži neka nit. Ako da (c!=0), proverava se trenutni broj niti koje pišu; ako je broj niti koje pišu 0 (dakle postoji brava čitanja) ili nit koja drži bravu nije trenutna nit, vraća se neuspeh (vezano za realizaciju pravedne i nepravedne brave).
- Ako broj brava pisanja prelazi maksimum (65535, 2^16-1), baca se Error.
- Ako je broj niti koje pišu 0 (i niti koje čitaju bi onda trebalo da je 0, jer je slučaj
c!=0već obrađen), a trenutna nit treba da se blokira, vraća se neuspeh; ako CAS-om ne uspe povećanje broja niti koje pišu, takođe se vraća neuspeh. - Ako je c=0, w=0 ili c>0, w>0 (reentrantnost), postavlja se trenutna nit ili vlasnik brave i vraća uspeh!
tryAcquire() pored uslova reentrantnosti (trenutna nit je nit koja je dobila bravu pisanja) dodaje i proveru postojanja brave čitanja. Ako postoji brava čitanja, brava pisanja ne može da se dobije, iz razloga što se mora osigurati da operacije brave pisanja budu vidljive za bravu čitanja; kada bi se dozvolilo dobijanje brave pisanja dok je brava čitanja već dobijena, druge niti koje trenutno čitaju ne bi mogle da opaze operaciju niti koja piše.
Zato brava pisanja može biti dobijena tek kad sve druge niti koje čitaju puste bravu čitanja; a jednom dobijena, sve naredne niti koje čitaju ili pišu se blokiraju. Puštanje brave pisanja je u suštini slično kao u ReentrantLock-u — svako puštanje smanjuje stanje pisanja; kada stanje pisanja postane 0, brava pisanja je puštena i niti koje čekaju mogu nastaviti sa radom, a izmene prethodne niti koje je pisala postaju vidljive.
Zatim kod brave čitanja:
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1; // ako je neka druga nit već dobila bravu pisanja, trenutna nit ne uspeva da dobije bravu čitanja i prelazi u stanje čekanja
int r = sharedCount(c);
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}Vidimo da u metodi tryAcquireShared(int unused), ako je neka druga nit već dobila bravu pisanja, trenutna nit ne uspeva da dobije bravu čitanja i prelazi u stanje čekanja. Ako je trenutna nit dobila bravu pisanja ili brava pisanja nije dobijena, trenutna nit (na nit-bezbedan način, uz CAS) povećava stanje čitanja i uspešno dobija bravu čitanja. Svako puštanje brave čitanja (nit-bezbedno, jer više niti koje čitaju može istovremeno puštati bravu) smanjuje stanje čitanja za vrednost „1<<16". Zato brava za čitanje/pisanje omogućava deljenje procesa čitanje-čitanje, a procesi čitanje-pisanje, pisanje-čitanje i pisanje-pisanje su međusobno isključivi.
Sada se vratimo na izvorni kod zaključavanja pravedne i nepravedne brave u mutex bravi ReentrantLock:

Primećujemo da ReentrantLock, iako ima i pravednu i nepravednu varijantu, u oba slučaja dodaje isključivu bravu. Prema izvornom kodu, kad neka nit pozove lock da dobije bravu, ako resurs nije zaključan drugom niti, trenutna nit nakon uspešnog CAS ažuriranja state preuzima taj resurs. Ako je resurs zauzet i to ne od strane trenutne niti, zaključavanje ne uspeva. Zato je jasno da ReentrantLock, bilo za čitanje ili pisanje, uvek dodaje isključivu bravu.
Ukratko, samo synchronized daleko nije dovoljan za raznolike poslovne zahteve prema bravama. Sledeće ćemo predstaviti neke interfejse i klase brava iz JDK-a.
Brave u JUC paketu
Kao što je poznato, klase vezane za istovremeni rad u JDK-u uglavnom se nalaze u JUC paketu.

Po imenu se vidi da paket locks pruža alatke za istovremene brave. AQS (AbstractQueuedSynchronizer), o kom smo ranije govorili, nalazi se u tom paketu.
Apstraktne klase AQS/AQLS/AOS
Ove tri apstraktne klase su u međusobnoj vezi, pa ih obradimo zajedno.
Prvo, AQS (AbstractQueuedSynchronizer), objavljen uz JDK 1.5, pruža osnovnu implementaciju „red-sinhronizatora".
„Resurs" u AQS-u predstavljen je jednim podatkom tipa int; kako poslovni zahtevi ponekad zahtevaju broj resursa koji prelazi opseg int, u JDK 1.6 uveden je AQLS (AbstractQueuedLongSynchronizer). Njegov kod je gotovo identičan AQS-u, samo je tip resursa long.

I AQS i AQLS nasleđuju klasu AOS (AbstractOwnableSynchronizer). I ona se pojavila u JDK 1.6.

Ova klasa ima samo nekoliko linija koda. Iz komentara na izvornoj klasi vidimo da služi da izrazi odnos između brave i njenog vlasnika (ekskluzivni režim). Pogledajmo njene glavne metode:
// Ekskluzivni režim, vlasnik brave
private transient Thread exclusiveOwnerThread;
// Postavljanje vlasnika brave
protected final void setExclusiveOwnerThread(Thread t) {
exclusiveOwnerThread = t;
}
// Dohvatanje niti koja drži bravu
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}Interfejsi Condition/Lock/ReadWriteLock
U paketu locks postoje tri interfejsa: Condition, Lock, ReadWriteLock.
Od njih, Lock i ReadWriteLock već po imenu otkrivaju značenje — brava i brava za čitanje/pisanje. Interfejs Lock sadrži metode za dobijanje i puštanje brave, dok ReadWriteLock ima samo dve metode koje vraćaju „bravu za čitanje" i „bravu za pisanje":
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}Interfejs Lock ima metodu kojom se dobija Condition (o kom će kasnije biti detaljno reči — kliknite na link):
Condition newCondition();Ranije smo pomenuli da svaki objekat može preko metoda Object-a wait/notify da ostvari mekanizam čekanje/obaveštenje. Condition interfejs nudi slične metode kao Object i može se u kombinaciji sa Lock-om koristiti za obrasce čekanje/obaveštenje.
Kad već postoje metode nadgledanja objekta, zašto koristiti Condition? Kratko poređenje:
| Stavka poređenja | Nadgledač Object | Condition |
|---|---|---|
| Preduslov | Dobiti bravu objekta | Pozvati Lock.lock za bravu, pozvati Lock.newCondition za Condition objekat |
| Način poziva | Direktno, npr. object.notify() | Direktno, npr. condition.await() |
| Broj redova čekanja | Jedan | Više |
| Trenutna nit pušta bravu i prelazi u čekanje | Podržano | Podržano |
| Trenutna nit pušta bravu i prelazi u čekanje bez prekida tokom čekanja | Nije podržano | Podržano |
| Trenutna nit pušta bravu i prelazi u čekanje sa vremenskim ograničenjem | Podržano | Podržano |
| Trenutna nit pušta bravu i prelazi u čekanje do nekog budućeg trenutka | Nije podržano | Podržano |
| Budi jednu nit iz reda čekanja | Podržano | Podržano |
| Budi sve niti iz reda čekanja | Podržano | Podržano |
Condition je u suštini sličan wait/notify iz Object-a. Metoda await iz Condition odgovara metodi wait iz Object, a signal/signalAll iz Condition odgovara notify/notifyAll() iz Object. Ali Condition je neka vrsta pojačane verzije Object-ovog mehanizma čekanje/obaveštenje. Pogledajmo glavne metode:
| Naziv metode | Opis |
|---|---|
await() | Trenutna nit prelazi u čekanje dok ne bude obaveštena (signal) ili prekinuta; trenutna nit prelazi u izvršavanje i vraća se iz await() kada: (1) druga nit pozove signal/signalAll nad istim Condition objektom i trenutna nit bude probuđena; (2) druga nit pozove interrupt i prekine trenutnu nit; |
awaitUninterruptibly() | Trenutna nit prelazi u čekanje dok ne bude obaveštena, pri čemu je neosetljiva na signale prekida i ne podržava prekid trenutne niti |
| awaitNanos(long) | Trenutna nit prelazi u čekanje dok ne bude obaveštena, prekinuta ili dok ne istekne vreme. Ako je povratna vrednost ≤ 0, smatra se da je vreme isteklo |
| awaitUntil(Date) | Trenutna nit prelazi u čekanje dok ne bude obaveštena, prekinuta ili dok ne istekne vreme. Ako je obaveštena pre zadatog vremena, vraća true, inače false |
| signal() | Budi jednu nit koja čeka na Condition; probuđena nit pre povratka iz metode mora da dobije bravu vezanu za Condition objekat |
| signalAll() | Budi sve niti koje čekaju na Condition; niti koje se vraćaju iz await() i sl. moraju prvo da dobiju bravu vezanu za Condition objekat |
Reentrantna brava ReentrantLock
ReentrantLock (o kom će sledećem biti reči — kliknite na link) podrazumevana je implementacija interfejsa Lock i implementira osnovne funkcije brave.
Po imenu, to je „reentrantna" brava; iz izvornog koda, interno sadrži apstraktnu klasu Sync koja nasleđuje AQS i sama realizuje sinhronizator.
ReentrantLock interno ima i dve neapstraktne klase, NonfairSync i FairSync, koje nasleđuju Sync. Iz imena se vidi da su to „nepravedni sinhronizator" i „pravedni sinhronizator". Dakle, ReentrantLock podržava i „pravednu" i „nepravednu" bravu.
Iz izvornog koda ta dva sinhronizatora vidi se da su im realizacije „ekskluzivne". Obje pozivaju metod setExclusiveOwnerThread iz AOS-a, pa je ReentrantLock-ova brava „ekskluzivna", odnosno „isključiva" i ne može se deliti.
U konstruktoru ReentrantLock-a može se proslediti parametar tipa boolean kojim se određuje da li je u pitanju pravedna brava; podrazumevano je nepravedna. Taj parametar se nakon instanciranja ne može menjati, a može se pročitati metodom isFair().
Pogledajmo jednostavan primer ReentrantLock-a:
public class Counter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock(); // dobij bravu
try {
count++;
System.out.println("Uvećanje " + Thread.currentThread().getName() + ": " + count);
} finally {
lock.unlock(); // pusti bravu
}
}
public static void main(String[] args) {
Counter counter = new Counter();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Konačan rezultat: " + counter.count);
}
}U ovom primeru klasa Counter koristi ReentrantLock da zaštiti pristup promenljivoj count. Metod increment prvo dobija bravu, zatim uvećava brojač, a u finally bloku pušta bravu. Time se osigurava da, čak i ako metod izbaci izuzetak, brava bude ispravno puštena.
U main metodi pokrećemo dve niti koje istovremeno izvršavaju operaciju increment. Zbog brave, pristup promenljivoj count je serijalizovan i rezultat je ispravan.
Ovaj primer prikazuje osnovnu upotrebu ReentrantLock-a. U poređenju sa ključnom reči synchronized, ReentrantLock pruža veću fleksibilnost — prekidivo dobijanje brave, opciju pravedne brave, vremenski ograničeno dobijanje brave itd.
Pogledajmo konačni izlaz:

Brava za čitanje/pisanje ReentrantReadWriteLock
ReentrantReadWriteLock (o kom će kasnije biti detaljno reči — kliknite na link) podrazumevana je implementacija interfejsa ReadWriteLock. Funkcionalno je sličan ReentrantLock-u: takođe je reentrantan, podržava i nepravednu i pravednu bravu. Razlika je u tome što dodatno podržava i „bravu za čitanje/pisanje".
Interna struktura ReentrantReadWriteLock-a je grubo ovakva:
// Interna struktura
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
// konkretne implementacije
}
static final class NonfairSync extends Sync {
// konkretne implementacije
}
static final class FairSync extends Sync {
// konkretne implementacije
}
public static class ReadLock implements Lock, java.io.Serializable {
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
// konkretne implementacije
}
public static class WriteLock implements Lock, java.io.Serializable {
private final Sync sync;
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
// konkretne implementacije
}
// Konstruktor, inicijalizuje dve brave
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
// Metode za dobijanje brave čitanja i brave pisanja
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }Vidimo da interno održava dva sinhronizatora i dve implementacije Lock-a: ReadLock i WriteLock. Iz izvornog koda se vidi da te dve interne klase koriste sinhronizator spoljašnje klase.
Pogledajmo primer upotrebe ReentrantReadWriteLock-a:
public class SharedResource {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private int data = 0;
public void write(int value) {
lock.writeLock().lock(); // dobij bravu pisanja
try {
data = value;
System.out.println("Pisanje " + Thread.currentThread().getName() + ": " + data);
} finally {
lock.writeLock().unlock(); // pusti bravu pisanja
}
}
public void read() {
lock.readLock().lock(); // dobij bravu čitanja
try {
System.out.println("Čitanje " + Thread.currentThread().getName() + ": " + data);
} finally {
lock.readLock().unlock(); // pusti bravu čitanja
}
}
public static void main(String[] args) {
SharedResource sharedResource = new SharedResource();
// kreiraj niti za čitanje
Thread readThread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
sharedResource.read();
}
});
Thread readThread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
sharedResource.read();
}
});
// kreiraj nit za pisanje
Thread writeThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
sharedResource.write(i);
}
});
readThread1.start();
readThread2.start();
writeThread.start();
try {
readThread1.join();
readThread2.join();
writeThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}U gornjem kodu definišemo klasu SharedResource koja koristi ReentrantReadWriteLock da zaštiti svoje interne podatke. Metod write dobija bravu pisanja i ažurira deljene podatke. Metod read dobija bravu čitanja i čita deljene podatke.
U main metodi kreiramo dve niti za čitanje i jednu za pisanje. Pošto ReentrantReadWriteLock dozvoljava istovremeno više operacija čitanja, niti za čitanje mogu raditi paralelno. Međutim, operacije pisanja se serijalizuju i tokom pisanja operacije čitanja se blokiraju.
Pogledajmo izlaz:
Čitanje Thread-0: 0
Čitanje Thread-1: 0
Pisanje Thread-2: 0
Pisanje Thread-2: 1
Pisanje Thread-2: 2
Pisanje Thread-2: 3
Pisanje Thread-2: 4
Čitanje Thread-0: 4
Čitanje Thread-1: 4
Čitanje Thread-0: 4
Čitanje Thread-1: 4
Čitanje Thread-0: 4
Čitanje Thread-1: 4
Čitanje Thread-0: 4
Čitanje Thread-1: 4ReentrantReadWriteLock implementira bravu za čitanje/pisanje, ali ima jedan mali nedostatak: tokom operacije „pisanja" druge niti ne mogu ni da pišu ni da čitaju. Taj fenomen nazivamo „izgladnjivanjem pisanja", a raspravljamo o njemu u nastavku uz klasu StampedLock.
Kralj brava StampedLock
Klasa StampedLock objavljena je u Javi 8, a napisao ju je veliki majstor Doug Lea; neki je zovu kraljem performansi među bravama.
StampedLock ne implementira ni interfejs Lock ni interfejs ReadWriteLock, ali implementira funkciju „brave za čitanje/pisanje" i uz to ima veće performanse od ReentrantReadWriteLock. StampedLock još podeljuje bravu čitanja na „optimističku bravu čitanja" i „pesimističku bravu čitanja".
Rekli smo da ReentrantReadWriteLock pati od „izgladnjivanja pisanja", dok StampedLock ne pati. Kako to postiže?
Njegova osnovna ideja je: ako tokom čitanja dođe do pisanja, novu vrednost treba dobiti ponavljanjem, umesto da se blokira operacija pisanja. Ovaj obrazac je tipična ideja programiranja bez brava, ista kao CAS spin. Takav način rada čini StampedLock veoma pogodnim za scenarije sa mnogo niti koje čitaju i malo niti koje pišu, a uz to izbegava izgladnjivanje pisanja.
Analizirajmo zvaničan primer upotrebe (nalazi se iznad deklaracije klase u JDK izvornom kodu ili u Javadoc-u).

Pogledajmo.
class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
// Upotreba brave pisanja
void move(double deltaX, double deltaY) {
long stamp = sl.writeLock(); // dobij bravu pisanja
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp); // pusti bravu pisanja
}
}
// Upotreba optimističke brave čitanja
double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead(); // dobij optimističku bravu čitanja
double currentX = x, currentY = y;
if (!sl.validate(stamp)) { // //proveri da li je nakon optimističke brave čitanja došlo do neke brave pisanja; ako jeste, vraća false
stamp = sl.readLock(); // dobij pesimističku bravu čitanja
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp); // pusti pesimističku bravu čitanja
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
// Upotreba pesimističke brave čitanja i nadgradnje brave čitanja u bravu pisanja
void moveIfAtOrigin(double newX, double newY) {
long stamp = sl.readLock(); // pesimistička brava čitanja
try {
while (x == 0.0 && y == 0.0) {
// Brava čitanja proba da se pretvori u bravu pisanja: ako uspe, ekvivalentno dobijanju brave pisanja; ako ne uspe, znači da je brava pisanja zauzeta
long ws = sl.tryConvertToWriteLock(stamp);
if (ws != 0L) { // ako je konverzija uspela
stamp = ws; // oznaka brave čitanja menja se u oznaku brave pisanja
x = newX;
y = newY;
break;
}
else { // ako konverzija nije uspela
sl.unlockRead(stamp); // pusti bravu čitanja
stamp = sl.writeLock(); // prisilno dobij bravu pisanja
}
}
} finally {
sl.unlock(stamp); // pusti sve brave
}
}
}Optimistička brava čitanja podrazumeva pretpostavku da se deljena promenljiva neće promeniti tokom perioda dok je dobijena; pošto se pretpostavlja da se neće menjati, ne treba ni zaključavati.
Nakon dobijanja optimističke brave čitanja obaveze se neke operacije, a zatim se poziva metod validate, koji proverava da li je nakon tryOptimisticRead izvršena neka operacija pisanja; ako jeste, dobija se pesimistička brava čitanja, koja je, kao i brava čitanja u ReentrantReadWriteLock-u, deljiva brava.
Vidimo da StampedLock pri dobijanju brave vraća promenljivu tipa long, koja se pri puštanju brave prosleđuje nazad. Kratko pogledajmo izvorni kod:
// Za rad sa state nakon čega se dobija vrednost stamp
private static final int LG_READERS = 7;
private static final long RUNIT = 1L; //0000 0000 0001
private static final long WBIT = 1L << LG_READERS; //0000 1000 0000
private static final long RBITS = WBIT - 1L; //0000 0111 1111
private static final long RFULL = RBITS - 1L; //0000 0111 1110
private static final long ABITS = RBITS | WBIT; //0000 1111 1111
private static final long SBITS = ~RBITS; //1111 1000 0000
// Vrednost state pri inicijalizaciji
private static final long ORIGIN = WBIT << 1; //0001 0000 0000
// Deljena promenljiva state brave
private transient volatile long state;
// Za smeštanje viška brava čitanja kada dođe do preliva
private transient int readerOverflow;StampedLock koristi prvih 7 bita (LG_READERS) ove long promenljive da predstavi bravu čitanja: svako dobijanje pesimističke brave čitanja dodaje 1 (RUNIT), svako puštanje oduzima 1. Pošto pesimističkih brava čitanja može biti najviše 128 (ograničenje od 7 bitova), lako dolazi do preliva, pa se int promenljiva koristi za smeštanje onih koje preliju.
Brava pisanja se predstavlja preostalim bitovima promenljive state: svako dobijanje brave pisanja dodaje 0000 1000 0000 (WBIT). Važno je napomenuti da brava pisanja pri puštanju ne oduzima WBIT, već opet dodaje WBIT. To služi da svaka brava pisanja ostavi trag, čime se rešava ABA problem iz CAS-a i pruža osnova za optimističku bravu proveru promena — metod validate.
Optimistička brava čitanja je jednostavnija: ne menja stvarno vrednost state, već pri dobijanju brave pamti stanje pisanja iz state, a nakon završenih operacija proverava da li je deo state-a koji se odnosi na pisanje promenjen; kao što je već rečeno, svaka brava pisanja ostavlja trag, što ovde olakšava proveru promena.
Ukratko, performanse StampedLock-a su izvanredne i on može uglavnom da zameni ReentrantReadWriteLock. Hajde da vidimo primer uporedne upotrebe StampedLock-a i ReentrantReadWriteLock-a.
ReentrantReadWriteLock:
public class SharedResourceWithReentrantReadWriteLock {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private int data = 0;
public void write(int value) {
lock.writeLock().lock();
try {
data = value;
} finally {
lock.writeLock().unlock();
}
}
public int read() {
lock.readLock().lock();
try {
return data;
} finally {
lock.readLock().unlock();
}
}
public static void main(String[] args) {
SharedResourceWithReentrantReadWriteLock sharedResource = new SharedResourceWithReentrantReadWriteLock();
Thread writer = new Thread(() -> {
for (int i = 0; i < 5; i++) {
sharedResource.write(i);
System.out.println("Write: " + i);
}
});
Thread reader = new Thread(() -> {
for (int i = 0; i < 5; i++) {
int value = sharedResource.read();
System.out.println("Read: " + value);
}
});
writer.start();
reader.start();
}
}StampedLock:
public class SharedResourceWithStampedLock {
private final StampedLock sl = new StampedLock();
private int data = 0;
public void write(int value) {
long stamp = sl.writeLock();
try {
data = value;
} finally {
sl.unlockWrite(stamp);
}
}
public int read() {
long stamp = sl.tryOptimisticRead();
int currentData = data;
if (!sl.validate(stamp)) {
stamp = sl.readLock();
try {
currentData = data;
} finally {
sl.unlockRead(stamp);
}
}
return currentData;
}
public static void main(String[] args) {
SharedResourceWithStampedLock sharedResource = new SharedResourceWithStampedLock();
Thread writer = new Thread(() -> {
for (int i = 0; i < 5; i++) {
sharedResource.write(i);
System.out.println("Write: " + i);
}
});
Thread reader = new Thread(() -> {
for (int i = 0; i < 5; i++) {
int value = sharedResource.read();
System.out.println("Read: " + value);
}
});
writer.start();
reader.start();
}
}Pogledajmo poređenje izlaza.

Reentrantnost: ReentrantReadWriteLock podržava reentrantnost — unutar jedne niti može se više puta dobiti brava čitanja ili brava pisanja. StampedLock ne podržava reentrantnost.
Optimistička brava čitanja: StampedLock nudi mehanizam optimističke brave čitanja koji dozvoljava niti da čita podatke kad nema operacija pisanja, čime se poboljšavaju performanse. ReentrantReadWriteLock nema takav mehanizam.
Degradacija brave: StampedLock podržava degradaciju iz brave pisanja u bravu čitanja, što u određenim scenarijima pruža dodatnu fleksibilnost. ReentrantReadWriteLock ne nudi takvu funkciju neposredno.
Složenost API-ja: Zbog optimističke brave čitanja i degradacije brave, API StampedLock-a je nešto složeniji i zahteva pažljiviju upotrebu da bi se izbegle mrtve blokade i drugi problemi. API ReentrantReadWriteLock-a je relativno intuitivniji i lakši za korišćenje.
U zaključku, StampedLock pruža veće performanse i fleksibilnost, ali sa sobom donosi složeniju upotrebu. ReentrantReadWriteLock je relativno jednostavniji i intuitivniji, posebno za scenarije bez visokoistovremenog čitanja.
Druge alatke u JUC paketu
Time su završen pregled interfejsa i klasa brava u paketu locks; ovde ćemo pomenuti i neke druge alatke iz JUC paketa, kao što su Semaphore, CountDownLatch, CyclicBarrier, Exchanger, Phaser itd. (o njima će takođe biti reči u klasama za komunikaciju).
Semaphore
Semaphore je brojački semafor; služi da ograniči broj niti koje mogu pristupiti određenim (fizičkim ili logičkim) resursima. Konstruktorom Semaphore-a može se zadati broj semafora i da li je pravedan.

Semaphore ima dve glavne metode: acquire() i release(). Metod acquire() pokušava da dobije semafor; ako ne uspe, blokira trenutnu nit dok neka druga nit ne pusti semafor. Metod release() otpušta jedan semafor i time budi jednu nit koja čeka.
Semaphore ima i metod tryAcquire() koji pokušava da dobije semafor; ako ne uspe, vraća false bez blokiranja trenutne niti.
Semaphore kontroliše broj istovremenih pristupa određenom resursu, ali sam po sebi ne garantuje nit-bezbednost — zato je za nit-bezbednost potrebno dodati i sinhronizacionu bravu.
Pogledajmo primer upotrebe Semaphore-a:
public class ResourcePool {
private final Semaphore semaphore;
public ResourcePool(int limit) {
this.semaphore = new Semaphore(limit);
}
public void useResource() {
try {
semaphore.acquire();
// korišćenje resursa
System.out.println("Resurs počeo da se koristi " + Thread.currentThread().getName());
Thread.sleep(1000); // simulacija vremena korišćenja resursa
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
System.out.println("Resurs pušten " + Thread.currentThread().getName());
}
}
public static void main(String[] args) {
ResourcePool pool = new ResourcePool(3); // ograničava 3 niti da istovremeno pristupaju resursu
for (int i = 0; i < 10; i++) {
new Thread(pool::useResource).start();
}
}
}Pogledajmo izlaz:
Resurs počeo da se koristi Thread-0
Resurs počeo da se koristi Thread-2
Resurs počeo da se koristi Thread-1
Resurs pušten Thread-0
Resurs pušten Thread-2
Resurs počeo da se koristi Thread-4
Resurs počeo da se koristi Thread-3
Resurs počeo da se koristi Thread-5
Resurs pušten Thread-1
Resurs počeo da se koristi Thread-6
Resurs počeo da se koristi Thread-8
Resurs počeo da se koristi Thread-7
Resurs pušten Thread-4
Resurs pušten Thread-3
Resurs pušten Thread-5
Resurs pušten Thread-8
Resurs pušten Thread-6
Resurs počeo da se koristi Thread-9
Resurs pušten Thread-7
Resurs pušten Thread-9CountDownLatch
CountDownLatch je sinhronizaciona alatka koja dozvoljava jednoj ili više niti da čekaju dok druge niti ne završe svoje operacije.
CountDownLatch ima brojač koji se metodom countDown() umanjuje za jedan, a metodom await() može blokirati trenutnu nit dok vrednost brojača ne postane 0.
CountDownLatch se obično koristi za upravljanje čekanjem niti — može naterati neku nit da čeka do kraja odbrojavanja, pa tek onda počne izvršenje.
Pogledajmo primer upotrebe CountDownLatch-a:
public class InitializationDemo {
public static void main(String[] args) throws InterruptedException {
// Kreira CountDownLatch sa odbrojavanjem 3
CountDownLatch latch = new CountDownLatch(3);
Thread service1 = new Thread(new Service("Servis 1", 2000, latch));
Thread service2 = new Thread(new Service("Servis 2", 3000, latch));
Thread service3 = new Thread(new Service("Servis 3", 4000, latch));
service1.start();
service2.start();
service3.start();
// Čeka da se svi servisi inicijalizuju
latch.await();
System.out.println("Svi servisi su spremni");
}
static class Service implements Runnable {
private final String name;
private final int timeToStart;
private final CountDownLatch latch;
public Service(String name, int timeToStart, CountDownLatch latch) {
this.name = name;
this.timeToStart = timeToStart;
this.latch = latch;
}
@Override
public void run() {
try {
Thread.sleep(timeToStart);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + " je spreman");
latch.countDown(); // umanji brojač
}
}
}U ovom primeru imamo tri servisa; svaki se pokreće u zasebnoj niti i zahteva izvesno vreme za inicijalizaciju. Glavna nit pomoću CountDownLatch-a čeka da sva tri servisa završe pokretanje, pa tek onda nastavlja. Svaki servis po završetku pokretanja poziva countDown(). Glavna nit poziva await() i čeka da odbrojavanje postane nula, a zatim nastavlja.
Pogledajmo rezultat:
Servis 1 je spreman
Servis 2 je spreman
Servis 3 je spreman
Svi servisi su spremniCyclicBarrier
CyclicBarrier je sinhronizaciona alatka koja dozvoljava grupi niti da međusobno čekaju dok ne stignu do zajedničke tačke prepreke (common barrier point).
CyclicBarrier se može koristiti u scenarijima gde više niti vrše proračune, a zatim se rezultati spajaju. Na primer, čuvamo u jednom Excelu sve bankovne transakcije korisnika, pri čemu svaki list sadrži transakcije jednog računa za proteklu godinu; sada treba izračunati prosečni dnevni promet. Prvo više niti obrađuje transakcije po svakom listu, a po završetku dobijamo proseke po listovima; zatim se, koristeći barrierAction, iz tih rezultata izračunava ukupni dnevni promet za čitav Excel.
Brojač CyclicBarrier-a može se resetovati metodom reset(), pa može obraditi scenarije ciklične upotrebe. Na primer, veliki zadatak podelimo u 10 malih i stavimo 10 niti da izvršavaju tih 10 malih zadataka; kad svih 10 završi, rezultate spojimo — tu koristimo CyclicBarrier.
CyclicBarrier ima i konstruktor sa argumentom kojim se zadaje Runnable; taj Runnable se izvršava kad brojač CyclicBarrier-a postane 0, čime se obavlja složeniji zadatak.
Pogledajmo primer upotrebe:
public class CyclicBarrierDemo {
public static void main(String[] args) {
int numberOfThreads = 3; // broj niti
CyclicBarrier barrier = new CyclicBarrier(numberOfThreads, () -> {
// Operacija koja se izvršava kad sve niti stignu do prepreke
System.out.println("Sve niti su stigle do prepreke, ulaze u sledeću fazu");
});
for (int i = 0; i < numberOfThreads; i++) {
new Thread(new Task(barrier), "Thread " + (i + 1)).start();
}
}
static class Task implements Runnable {
private final CyclicBarrier barrier;
public Task(CyclicBarrier barrier) {
this.barrier = barrier;
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " čeka na prepreki");
barrier.await(); // čeka da sve niti stignu do prepreke
System.out.println(Thread.currentThread().getName() + " je prošao prepreku.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}Izlaz izgleda ovako:
Thread 1 čeka na prepreki
Thread 3 čeka na prepreki
Thread 2 čeka na prepreki
Sve niti su stigle do prepreke, ulaze u sledeću fazu
Thread 2 je prošao prepreku.
Thread 1 je prošao prepreku.
Thread 3 je prošao prepreku.Exchanger
Exchanger je alatka za saradnju među nitima, koja služi za razmenu podataka između niti. Pruža jednu tačku sinhronizacije u kojoj dve niti mogu razmeniti svoje podatke. Te dve niti razmenjuju podatke preko metode exchange — ako prva nit prva pozove exchange, čekaće dok i druga nit ne pozove exchange; kad obe stignu do sinhronizacione tačke, mogu razmeniti podatke, prosleđujući jedna drugoj one koje su proizvele.
Exchanger se može koristiti u genetskim algoritmima, proveri podataka i sinhronizaciji podataka i sl.
Pogledajmo primer upotrebe:
public class ExchangerDemo {
public static void main(String[] args) {
Exchanger<String> exchanger = new Exchanger<>();
new Thread(() -> {
try {
String data1 = "data1";
System.out.println(Thread.currentThread().getName() + " razmenjuje " + data1);
Thread.sleep(1000); // simulacija trajanja obrade niti
String data2 = exchanger.exchange(data1);
System.out.println(Thread.currentThread().getName() + " dobio je " + data2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Thread 1").start();
new Thread(() -> {
try {
String data1 = "data2";
System.out.println(Thread.currentThread().getName() + " razmenjuje " + data1);
Thread.sleep(2000); // simulacija trajanja obrade niti
String data2 = exchanger.exchange(data1);
System.out.println(Thread.currentThread().getName() + " dobio je " + data2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Thread 2").start();
}
}Izlaz izgleda ovako:
Thread 1 razmenjuje data1
Thread 2 razmenjuje data2
Thread 2 dobio je data1
Thread 1 dobio je data2Phaser
Phaser je sinhronizaciona alatka koja omogućava više niti da u određenom trenutku zajednički završe zadatak.
Phaser se može shvatiti kao brojač niti koji tu vrednost može uvećavati ili umanjivati za jedan. Kad vrednost tog brojača postane 0, sve niti koje su se zaustavile čekajući na await() nastavljaju izvršenje.
Brojač Phaser-a može se dinamički ažurirati, povećavati ili smanjivati. Phaser nudi i metode koji nam pomažu da bolje kontrolišemo dolazak niti.
Pogledajmo primer upotrebe:
public class PhaserDemo {
public static void main(String[] args) {
Phaser phaser = new Phaser(3); // 3 niti zajednički završavaju zadatak
new Thread(new Task(phaser), "Thread 1").start();
new Thread(new Task(phaser), "Thread 2").start();
new Thread(new Task(phaser), "Thread 3").start();
}
static class Task implements Runnable {
private final Phaser phaser;
public Task(Phaser phaser) {
this.phaser = phaser;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " završio prvi korak");
phaser.arriveAndAwaitAdvance(); // čeka da ostale niti završe prvi korak
System.out.println(Thread.currentThread().getName() + " završio drugi korak");
phaser.arriveAndAwaitAdvance(); // čeka da ostale niti završe drugi korak
System.out.println(Thread.currentThread().getName() + " završio treći korak");
phaser.arriveAndAwaitAdvance(); // čeka da ostale niti završe treći korak
}
}
}Izlaz izgleda ovako:
Thread 1 završio prvi korak
Thread 2 završio prvi korak
Thread 3 završio prvi korak
Thread 3 završio drugi korak
Thread 1 završio drugi korak
Thread 2 završio drugi korak
Thread 1 završio treći korak
Thread 3 završio treći korak
Thread 2 završio treći korakZaključak
U ovom tekstu predstavili smo interfejse i klase brava u JUC paketu, uključujući Lock, ReadWriteLock, Condition, ReentrantLock, ReentrantReadWriteLock, StampedLock i dr. Takođe smo opisali i druge alatke JUC paketa: Semaphore, CountDownLatch, CyclicBarrier, Exchanger, Phaser i sl.
Interfejsi i klase brava u JUC paketu čine jezgro Java istovremenog programiranja i često su tema na intervjuima, zato ih svakako valja dobro savladati.
Urednik: Chenmo Wang Er; sadržaj pre uređivanja potiče iz open-source repozitorijuma koji je prijatelj Xiao Qi Yinghuochong objavio: Duboko i pristupačno o Java višenitnosti, toplo preporučujemo; deo sadržaja potiče i iz ovog teksta inženjera Meituan Dianpinga Jia Qi-ja, takođe toplo preporučujemo.
