Šta tačno zaključava synchronized? Šta su biasovana brava, laka brava i teška brava?
U prethodnom poglavlju smo obradili osnovnu upotrebu ključne reči synchronized. Ona može da sinhronizuje metode i blokove koda — pa šta tačno synchronized zaključava? Sa nadogradnjom JDK verzija, koje je promove doneo synchronized? Da li je glasina da „synchronized ima loše performanse" zaista tačna?
Mislim da to zanima mnoge.
Prvo što treba jasno reći: Java brave za više niti se zasnivaju na objektima — u Javi svaki objekat može da posluži kao brava.
Takođe treba napomenuti da brava klase koju često čujemo zapravo jeste brava objekta. Govorili smo o tome u prethodnom poglavlju, sigurno su mnogi primetili.
Hajde da kažemo još par reči. Class objekat je posebna Java objekat koji predstavlja klase i interfejse u programu. Svaki tip u Javi (uključujući klase, interfejse, nizove i osnovne tipove) ima odgovarajući jedinstveni Class objekat u JVM-u. Taj Class objekat se kreira kada JVM učita klasu, i to JVM obavlja automatski.
Class objekat sadrži mnogo informacija vezanih za klasu, kao što su ime klase, nadklasa klase, interfejsi koje klasa implementira, konstruktori klase, metode klase, polja klase itd. Te informacije se obično nazivaju metapodacima (metadata).
Preko Class objekta mogu se dobiti metapodaci klase, pa čak i dinamički kreirati instance klase, pozivati metode klase, pristupati poljima klase itd. To je mekanizam refleksije (Reflection) u Javi.
Zato ono što često zovemo brava klase zapravo je brava Class objekta.
Osnovna upotreba brave
synchronized prevedeno na srpski znači „sinhronizovano".
Obično koristimo ključnu reč synchronized da zaključamo deo koda ili metod. U prethodnom poglavlju smo već o tome govorili, ovde ćemo kratko podsetiti, jer je synchronized zaista veoma važan — često se pita na intervjuu, a često se i koristi u razvoju. Obično ima sledeća tri oblika:
// Ključna reč na instancnoj metodi, brava je trenutna instanca
public synchronized void instanceLock() {
// code
}
// Ključna reč na statičkoj metodi, brava je trenutni Class objekat
public static synchronized void classLock() {
// code
}
// Ključna reč na bloku koda, brava je objekat u zagradama
public void blockLock() {
Object o = new Object();
synchronized (o) {
// code
}
}Ovde ćemo predstaviti koncept „kritične sekcije". „Kritična sekcija" je deo koda koji u istom trenutku može izvršavati samo jedna nit. U gore navedenom primeru, ako je ključna reč synchronized na metodi, kritična sekcija je cela unutrašnjost metode. Ako je u pitanju synchronized blok koda, kritična sekcija je oblast unutar bloka koda.
Iz gore navedenog primera se vidi da su sledeća dva načina pisanja zapravo ekvivalentna:
// Ključna reč na instancnoj metodi, brava je trenutna instanca
public synchronized void instanceLock() {
// code
}
// Ključna reč na bloku koda, brava je objekat u zagradama
public void blockLock() {
synchronized (this) {
// code
}
}Isto tako, i sledeće dve metode bi trebalo da budu ekvivalentne:
// Ključna reč na statičkoj metodi, brava je trenutni Class objekat
public static synchronized void classLock() {
// code
}
// Ključna reč na bloku koda, brava je objekat u zagradama
public void blockLock() {
synchronized (this.getClass()) {
// code
}
}Četiri stanja brave i degradacija brave
Pre JDK 1.6, sve brave su bile „teške" brave, jer su koristile mutex brave operativnog sistema. Kada jedna nit drži bravu, druge niti koje pokušavaju da uđu u synchronized blok biće blokirane dok bravu ne bude pušteno. Tu su uključeni prebacivanje konteksta niti i prebacivanje između korisničkog i kernel režima, pa je efikasnost niža.
To je i razlog zašto mnogi programeri smatraju da synchronized ima loše performanse.
Da bi se smanjila potrošnja performansi pri pribavljanju i puštanju brave, JDK 1.6 je uveo koncepte „biasovane brave" i „lake brave" i izvršio veliku nadogradnju synchronized-a, nakon koje su se performanse synchronized-a popne na novi nivo.
U JDK 1.6 i kasnijim verzijama, objekat zapravo ima četiri stanja brave, koja su po nivou od nižeg ka višem:
- Stanje bez brave
- Stanje biasovane brave
- Stanje lake brave
- Stanje teške brave
Stanje bez brave znači da resurs nije zaključan i da bilo koja nit može pokušati da ga izmeni — lako za razumevanje.
Razne brave se postepeno unapređuju u zavisnosti od situacije sukoba. Unapređenje brave se lako dešava, ali su uslovi za degradaciju brave stroži. Degradacija brave se dešava tokom Stop The World (važan koncept u Java sakupljanju smeća, o čemu će detaljno biti reči u poglavlju o JVM-u). Kada JVM uđe u safe point, proverava da li postoje neiskorišćene brave i zatim vrši degradaciju.
O degradaciji brave treba reći sledeće:
Za razliku od tvrdnje većine članaka da brava ne može da se degradira, HotSpot JVM zapravo podržava degradaciju brave. U ovoj objavi nalazi se veoma važna tvrdnja koju je dao R.
In its current implementation, monitor deflation is performed during every STW pause, while all Java threads are waiting at a safepoint. We have seen safepoint cleanup stalls up to 200ms on monitor-heavy-applications.
Gruba ideja je da se degradacija teške brave dešava u STW (Stop The World) fazi, a degradiraju se objekti kojima može pristupiti samo VMThread, a kojima ne pristupa nijedna druga JavaThread.
Upoređivanje prednosti i mana raznih brava (iz „Umetnosti Java konkurentnog programiranja"):
| Brava | Prednosti | Mane | Scenariji primene |
|---|---|---|---|
| Biasovana brava | Zaključavanje i otključavanje ne zahtevaju dodatnu potrošnju; razlika u odnosu na ne-sinhronizovanu metodu je na nivou nanosekundi. | Ako postoji sukob oko brave između niti, donosi dodatnu potrošnju na opoziv brave. | Za scenario u kojem samo jedna nit pristupa sinhronizovanom bloku. |
| Laka brava | Niti u sukobu se ne blokiraju, što poboljšava brzinu odziva programa. | Ako nikako ne dobije bravu, nit koja je u sukobu troši CPU spinning-om. | Zahtev za brzinom odziva. Sinhronizovani blok se izvršava veoma brzo. |
| Teška brava | Niti u sukobu ne koriste spinning, ne troše CPU. | Niti se blokiraju, vreme odziva je sporo. | Zahtev za propusnošću. Sinhronizovani blok se izvršava duže. |
Gde se nalazi brava objekta
Pomenuli smo da se Java brave zasnivaju na objektima.
Hajde prvo da pogledamo gde se čuva „brava" jednog objekta.
Svaki Java objekat ima zaglavlje objekta. Ako nije u pitanju niz, zaglavlje objekta se čuva u 2 širine reči; ako je niz, koristi se 3 širine reči. Na 32-bitnom procesoru jedna širina reči je 32 bita; na 64-bitnoj virtuelnoj mašini jedna širina reči je 64 bita. Sadržaj zaglavlja objekta je u sledećoj tabeli:
| Dužina | Sadržaj | Opis |
|---|---|---|
| 32/64bit | Mark Word | Čuva hashCode objekta ili informacije o bravi itd. |
| 32/64bit | Class Metadata Address | Pokazivač na podatke o tipu objekta |
| 32/64bit | Array length | Dužina niza (ako je niz) |
Hajde da pogledamo format Mark Word-a:
| Stanje brave | 29 bit ili 61 bit | 1 bit da li je biasovana brava? | 2 bit zastavice brave |
|---|---|---|---|
| Bez brave | 0 | 01 | |
| Biasovana brava | ID niti | 1 | 01 |
| Laka brava | Pokazivač na zapis brave u stack-u | Ovaj bit se ne koristi za obeležavanje biasovane brave | 00 |
| Teška brava | Pokazivač na mutex (tešku bravu) | Ovaj bit se ne koristi za obeležavanje biasovane brave | 10 |
| GC oznaka | Ovaj bit se ne koristi za obeležavanje biasovane brave | 11 |
Vidi se da kada je stanje objekta biasovana brava, Mark Word čuva biasovani ID niti; kada je stanje laka brava, Mark Word čuva pokazivač na Lock Record u stack-u niti; kada je stanje teška brava, Mark Word je pokazivač na objekat monitora u heap-u.
U Javi je monitor (nadzornik) alatka za sinhronizaciju koja štiti deljene podatke i sprečava nekonzistentnost podataka usled istovremenog pristupa više niti. U Javi svaki objekat ima ugrađeni monitor.
Monitor obuhvata dva važna dela: jedno je brava, a drugo je mehanizam čekanje/obaveštenje. Ovo drugo se implementira preko metoda wait(), notify(), notifyAll() iz klase Object (detaljno ćemo obraditi kada budemo pričali o Condition-u i modelu proizvođač-potrošač).
U nastavku su posebno opisane ove brave i kako se međusobno unapređuju.
Biasovana brava
Autor Hotspot-a je kroz prethodna istraživanja otkrio da u većini slučajeva brava ne samo da nema sukob između više niti, već je uvek više puta pribavlja ista nit, pa je uveo biasovanu bravu.
Biasovana brava je naklonjena prvoj niti kojoj pristupi bravi. Ako tokom daljeg izvršavanja ovoj bravi ne pristupi nijedna druga nit, nit koja drži biasovanu bravu nikada neće morati da aktivira sinhronizaciju. Odnosno, biasovana brava u situaciji bez sukoba oko resursa eliminiše sinhronizovane naredbe — čak ni CAS (detaljno opisan kasnije, kliknite na link) operacije se ne obavljaju, što znatno poboljšava performanse programa.
Prosto rečeno, za bravu se postavi promenljiva; ako je true, znači da nema sukoba oko resursa i da nije potrebno prolaziti kroz razne procedure zaključavanja/otključavanja. Ako je false, znači da druge niti konkurišu za resurs i da će se nastaviti dalji postupak.
Princip implementacije biasovane brave
Kada nit prvi put uđe u sinhronizovani blok, u zaglavlju objekta i u zapisu brave u stack frame-u se čuva ID niti kojoj je brava naklonjena. Kada ta nit sledeći put uđe u taj sinhronizovani blok, proverava da li u Mark Word-u brave stoji njen sopstveni ID niti.
Ako da, to znači da je ta nit već pribavila bravu i da prilikom sledećih ulazaka i izlazaka iz sinhronizovanog bloka ne mora da troši CAS operacije na zaključavanje i otključavanje. Ako ne, to znači da je druga nit došla da konkuriše za ovu biasovanu bravu. Tada se pokušava sa CAS-om da se zameni ID niti u Mark Word-u sa ID-jem nove niti. Ovde treba razlikovati dva slučaja:
- Uspelo, što znači da prethodna nit više ne postoji; ID niti u Mark Word-u postaje ID nove niti, brava se ne unapređuje i ostaje biasovana brava.
- Neuspelo, što znači da prethodna nit još uvek postoji; tada se pauzira prethodna nit, postavlja se zastavica biasovane brave na 0 i zastavica brave na 00, brava se unapređuje u laku bravu i nastavlja se takmičenje za bravu po principu lake brave.
CAS: Compare and Swap će biti detaljno obrađen kasnije; možete kliknuti na link za direktan pristup, ovde ćemo ga samo kratko pomenuti.
CAS znači „uporedi i postavi" i služi za pružanje atomskih operacija na hardverskom nivou. U nekim arhitekturama procesora (kao x86) upoređivanje i zamena se implementira instrukcijom CMPXCHG (Compare and Exchange, atomska instrukcija) — upoređuje se da li je vrednost jednaka zadatoj i ako jeste, menja se, a ako nije, ne menja se.
Proces takmičenja niti za biasovanu bravu je ispod:

Na slici je prikazano da lock record pokazivač pokazuje na najskoriji lock record u trenutnom stack-u, što je rezultat toga da je laka brava izvršila zaključavanje po principu prvi-došao-prvi-uslužen.
Opoziv biasovane brave
Biasovana brava koristi mehanizam puštanja brave tek kada se pojavi sukob, pa tek kada druge niti pokušaju da se takmiče za biasovanu bravu, nit koja drži biasovanu bravu pušta je.
Kada se biasovana brava unapređuje u laku bravu, pauzira se nit koja drži biasovanu bravu i resetuje zastavica biasovane brave. Ovaj proces izgleda jednostavno, ali je zapravo skup. Približan proces je sledeći:
- Na safe point-u (vremenskoj tački u kojoj se ne izvršava nijedan bajtkod) zaustavlja se nit koja drži bravu.
- Prolazi se kroz stack niti; ako postoje zapisi brave, potrebno je popraviti zapis brave i Mark Word tako da pređu u stanje bez brave.
- Budi se zaustavljena nit i trenutna brava se unapređuje u laku bravu.
Zato, ako su u aplikaciji sve brave obično u stanju sukoba, biasovana brava će biti teret. U tom slučaju možemo od samog početka isključiti podrazumevanu funkciju biasovane brave:
-XX:UseBiasedLocking=falseKlasična slika ispod sumira pribavljanje i opoziv biasovane brave:

Laka brava
Više niti u različitim vremenskim intervalima pribavlja istu bravu, odnosno ne postoji sukob oko brave, pa nema ni blokiranih niti. Za tu situaciju JVM koristi laku bravu da izbegne blokiranje i buđenje niti.
JVM će za svaku nit u stack frame-u trenutne niti kreirati prostor za čuvanje zapisa brave, koji nazivamo Displaced Mark Word. Ako nit pri pribavljanju brave otkrije da je u pitanju laka brava, kopiraće Mark Word brave u svoj Displaced Mark Word.
Zatim nit pokušava da CAS-om zameni Mark Word brave pokazivačem na zapis brave. Ako uspe, trenutna nit pribavlja bravu; ako ne uspe, to znači da je Mark Word već zamenjen zapisom brave druge niti, što ukazuje na to da se trenutna nit takmiči sa drugim nitima za bravu. Trenutna nit tada pokušava da pribavi bravu spinning-om.
Spinning: neprekidno pokušavanje pribavljanja brave, obično se implementira petljom.
Spinning troši CPU. Ako bravu nikako ne uspe da pribavi, ta nit će stalno biti u stanju spinninga i uzalud trošiti CPU resurse. Najjednostavnije rešenje ovog problema je zadati broj spinninga, na primer 10 puta, pa ako bravu i dalje ne pribavi, preći u stanje blokade.
Ali JDK je usvojio pametniji način — adaptivni spinning. Jednostavno rečeno, ako je nit spinningom uspela, sledeći put će spinning biti veći broj puta; ako je spinningom zakazala, broj spinninga će se smanjiti.
Spinning se ne nastavlja zauvek. Ako se do određenog stepena (zavisi od JVM-a i operativnog sistema) ne pribavi brava, to se naziva neuspelim spinningom i ta nit će biti blokirana. Istovremeno će ta brava biti unapređena u tešku bravu.
Puštanje lake brave
Pri puštanju brave, trenutna nit će CAS operacijom kopirati sadržaj Displaced Mark Word-a nazad u Mark Word brave. Ako nije bilo sukoba, ovo kopiranje će uspeti. Ako je neka druga nit zbog višestrukih spinninga unapredila laku bravu u tešku bravu, CAS operacija će neuspeti i tada će se brava pustiti i blokirane niti će se probuditi.
Jedna slika objašnjava proces zaključavanja i puštanja brave:

Teška brava
Teška brava se oslanja na mutex bravu operativnog sistema (mutex, služi da garantuje da u bilo kom trenutku samo jedna nit može izvršiti određeni segment koda), a prebacivanje stanja između niti u operativnom sistemu zahteva relativno dugo vreme, pa je teška brava neefikasna, ali blokirane niti ne troše CPU.
Rekli smo da svaki objekat može da posluži kao brava. Kada više niti istovremeno zatraži bravu nekog objekta, brava objekta će postaviti nekoliko stanja kojima se razlikuju niti koje traže bravu:
- Contention List: sve niti koje traže bravu će prvo biti smeštene u taj red za takmičenje.
- Entry List: niti iz Contention List-a koje ispunjavaju uslove za kandidate premeštaju se u Entry List.
- Wait Set: niti koje su blokirane pozivom metode wait smeštaju se u Wait Set.
- OnDeck: u svakom trenutku najviše jedna nit može da se takmiči za bravu; ta nit se zove OnDeck.
- Owner: nit koja je pribavila bravu se zove Owner.
- !Owner: nit koja je pustila bravu.
Kada nit pokuša da pribavi bravu, a ta brava je već zauzeta, ta nit će biti umotana u ObjectWaiter objekat i ubačena na početak reda Contention List, a zatim će se pozvati metoda park da se obustavi trenutna nit.
Kada nit pusti bravu, izabraće jednu nit iz Contention List-a ili EntryList-a da je probudi. Izabrana nit se zove Heir presumptive, odnosno pretpostavljeni naslednik. Nakon što se probudi, pretpostavljeni naslednik će pokušati da pribavi bravu, ali pošto synchronized nije fer, pretpostavljeni naslednik ne mora nužno da pribavi bravu.
To je zato što se kod teške brave, ako nit ne uspe da pribavi bravu, direktno prelazi u stanje blokade i čeka raspored operativnog sistema.
Ako nakon pribavljanja brave nit pozove Object.wait, ta nit se dodaje u WaitSet. Kada se probudi putem Object.notify, nit se premešta iz WaitSet-a u Contention List ili EntryList. Treba napomenuti da pri pozivu metode wait ili notify objekta brave, ako je trenutno stanje brave biasovana ili laka, prvo će se proširiti u tešku bravu.
Proces unapređenja brave
Svaka nit koja se sprema da pribavi deljeni resurs: Prvi korak, proverava da li u MarkWord-u stoji njen sopstveni ThreadId. Ako da, to znači da je trenutna nit u „biasovanoj bravi".
Drugi korak, ako MarkWord nije njen ThreadId, brava se unapređuje. Tada se CAS-om vrši promena: nova nit, na osnovu postojećeg ThreadId-a u MarkWord-u, obaveštava prethodnu nit da pauzira, a prethodna nit prazni sadržaj Markword-a.
Treći korak, obe niti kopiraju HashCode objekta brave u svoj novokreirani prostor za čuvanje zapisa brave, a zatim kroz CAS operaciju menjaju sadržaj MarkWord-a objekta brave na adresu svog novokreiranog zapisa brave, čime se takmiče za MarkWord.
Četvrti korak, nit koja uspešno izvrši CAS u trećem koraku pribavlja resurs, a one koje ne uspeju prelaze u spinning.
Peti korak, nit koja spinnuje u procesu spinninga uspešno pribavlja resurs (odnosno prethodna nit koja je držala resurs završava i pušta deljeni resurs), pa celo stanje i dalje ostaje stanje lake brave. Ako spinning ne uspe.
Šesti korak, prelazi u stanje teške brave. Tada nit koja spinnuje prelazi u blokadu i čeka da prethodna nit završi i probudi je.
Rezime
- Svaki objekat u Javi može da posluži kao brava; Java brave se zasnivaju na objektima.
- Ključna reč synchronized može da modifikuje metode i blokove koda; ona garantuje da u istom trenutku najviše jedna nit izvršava taj deo koda.
- Kada synchronized modifikuje metod, brava je trenutni objekat instance; kada modifikuje statičku metodu, brava je trenutni Class objekat; kada modifikuje blok koda, brava je objekat u zagradama.
- Java 6 je, da bi smanjila potrošnju performansi pri pribavljanju i puštanju brave, uvela „biasovanu bravu" i „laku bravu". Pre Jave 6, sve brave su bile „teške". Zato u Javi 6 i kasnijim, objekat zapravo ima četiri stanja brave, koja su po nivou od nižeg ka višem: stanje bez brave, stanje biasovane brave, stanje lake brave, stanje teške brave.
- Biasovana brava je naklonjena prvoj niti kojoj pristupi bravi. Ako joj tokom daljeg izvršenja ne pristupi nijedna druga nit, nit koja drži biasovanu bravu nikada neće morati da aktivira sinhronizaciju. Odnosno, biasovana brava u situaciji bez sukoba oko resursa eliminiše sinhronizovane naredbe, čak ni CAS operacije se ne obavljaju, čime se poboljšavaju performanse programa.
- Laka brava se implementira CAS operacijama i spinningom; ako spinning ne uspe, unapređuje se u tešku bravu.
- Teška brava se oslanja na mutex operativnog sistema, a prebacivanje stanja između niti u operativnom sistemu zahteva relativno dugo vreme, pa je teška brava neefikasna, ali blokirane niti ne troše CPU.
Urednik: Chenmo Wang Er. Originalni sadržaj potiče iz ovog repozitorijuma mog prijatelja Xiaoqi Yinghuochong-a: Java višenitnost jednostavno objašnjena, toplo preporučujem.
