Duboko i pristupačno o biased bravi
Pre JDK 1.5, suočavajući se sa Java problemima istovremenosti, synchronized bio je univerzalno rešenje:
- Sinhronizovana metoda, zaključava trenutni instancirani objekat
- Sinhronizovana statička metoda, zaključava Class objekat trenutne klase
- Sinhronizovani blok, zaključava objekat naveden u bloku koda
Uzmimo sinhronizovani blok kao primer:
public void test(){
synchronized (object) {
i++;
}
}Instrukcije nakon prevodjenja sa javap -v su sledeće:

- Instrukcija
monitorenterse nakon prevodjenja umeće na početak sinhronizovanog bloka koda; - Instrukcija
monitorexitse umeće na mesto kraja metoda i mesto izuzetaka (zapravo krije try-finally). - Svaki objekat ima pridružen monitor; kad nit izvrši do instrukcije monitorenter, dobija vlasništvo nad pridruženim
monitor-om, čime dobija bravu objekta.
Objekat nadgledač monitor
Ovde ukratko o pojmu monitor-a.
U Javi se monitor može shvatiti kao čuvar ili zaštitar koji osigurava da u svakom trenutku samo jedna nit može pristupiti zaštićenom delu koda. Možete ga zamisliti kao vrata sobe; u sobi se nalaze neke važne stvari, a monitor je zaštitar koji čuva ta vrata.
Monitor radi ovako:
- Ulazak u sobu: kad nit želi da uđe u zaštićeni deo koda (sobu), mora dobiti dozvolu monitor-a. Ako u sobi nema druge niti, monitor je pušta i zatvara vrata.
- Čekanje drugih niti: ako u sobi već postoji nit, druge niti moraju čekati. Monitor će ih pustiti da čekaju u redu dok nit u sobi ne završi posao i ne napusti je.
- Napuštanje sobe: kad nit završi posao i napusti zaštićeni deo koda, monitor ponovo otvara vrata i pušta sledeću nit iz reda čekanja.
- Koordinacija niti: monitor, putem nekih posebnih mehanizama (npr. metode wait i notify, o čemu će detaljnije biti reči uz Condition) koordinira saradnju među nitima. Nit preko monitor-a može signalizirati drugim nitima da sada mogu izvršiti određene operacije.
Teška brava
Kada druga nit stigne do sinhronizovanog bloka, pošto nema vlasništvo nad monitor-om, biva blokirana; tada kontrolu mora preuzeti operativni sistem, dakle dolazi do prebacivanja iz user mode u kernel mode (spomenuto je u tekstu o JMM — kliknite na link). Operativni sistem preuzima raspoređivanje niti i promene stanja niti, što zahteva često prebacivanje između ta dva režima (promena konteksta).
Pribegavati jezgru pri svakom takmičenju je loše, izaziva veliki overhead, pa se naziva teška brava, a time je i efikasnost niska. To je mnogima ostavilo dubok utisak da ključna reč synchronized ima lošije performanse u odnosu na druge mehanizame sinhronizacije — iako to nije tačno, kao što smo već objasnili.
Laka brava
Ako CPU preko CAS (detaljno kasnije — kliknite na link) može da obradi zaključavanje/oslobađanje brave, neće biti promene konteksta.
Ali kad je takmičenje veliko, uzalud je pokušavati CAS mnogo puta i trošiti CPU; u takvoj situaciji je bolje preći u tešku bravu i blokirati niti u redu za takmičenje, čime nastaje proces nadgradnje lake brave u tešku bravu.

Autori HotSpot-a su otkrili da u većini slučajeva brave ne samo da nemaju višenitno takmičenje, već ih uvek dobija ista nit, više puta. Ako uvek ista nit dobija bravu, a i dalje se koristi CAS, to i dalje košta; kako to smanjiti?
Biased brava
Biased brava zapravo znači da „objekat brave" podsvesti „naginje" istoj niti za pristup, tako da objekat brave zapamti ID te niti; kad nit sledeći put traži bravu, samo pokaže identitet i ako je isti ID, odmah je dobije. To je proces load-and-test, još lakši od CAS-a.
Ali u višenitnoj sredini ne može uvek ista nit dobijati tu bravu; i druge niti moraju raditi. Ako se pojavi takmičenje više niti, postoji proces nadgradnje biased brave.

Ovde možemo razmisliti: može li biased brava da zaobiđe laku bravu i direktno pređe u tešku?
U pitanju je uvek isti objekat brave sa više stanja brave, a cilj je očigledan:
Što manje resursa zauzima, program se brže izvršava.
I biased brava i laka brava ne pozivaju sistemski mutex (Mutex Lock); to su samo dodatna stanja brave uvedena radi boljih performansi, omogućavajući primenu najpogodnije strategije u različitim scenarijima:
- Biased brava: bez takmičenja, samo jedna nit ulazi u kritični deo — biased brava
- Laka brava: više niti naizmenično ulazi u kritični deo — laka brava
- Teška brava: više niti istovremeno pokušava ući u kritični deo — prepušta se sistemskom mutex-u
O ovome smo već govorili u tekstu o synchronized-u, ali ovu temu zaista vredi produbiti, pa ovde pristupamo iz drugog ugla i izdvajamo više vremena za nju.
Do ovde biste trebalo da razumete, ali i dalje će ostati mnoga pitanja:
- Gde objekat brave čuva ID niti?
- Kako se odvija čitav proces nadgradnje?
Da bismo razumeli ta pitanja, moramo prvo znati strukturu zaglavlja Java objekta.
Zaglavlje Java objekta
Prema uobičajenom shvatanju, prepoznavanje ID-a niti zahteva mapiranje; ako se to mapiranje posebno održava, mora se razmišljati i o nit-bezbednosti. Prema principu Okamove oštrice, u Javi je sve objekat i svaki objekat može poslužiti kao brava, pa je umesto zasebnog održavanja mapiranja bolje centralizovano čuvati podatke o bravi u samom Java objektu.
Princip Okamove oštrice je načelo rešavanja problema; jednostavno rečeno: pri objašnjenju nečega ne treba pretpostavljati više no što je potrebno; kad postoji više objašnjenja, izabrati ono sa najmanje pretpostavki, najjednostavnije.
Zaglavlje Java objekta sastoji se najviše od tri dela:
- MarkWord
- ClassMetadata Address
- Array Length (samo ako je objekat niz)
Od toga, Markword čuva ključno stanje brave; stanje brave objekta može se nadograđivati od biased brave preko lake brave do teške brave; uz početno stanje bez brave, možemo govoriti o ukupno 4 stanja. Da bi se toliko informacija izrazilo u jednom objektu, prirodno se koriste bitovi. U 64-bitnom operativnom sistemu to izgleda ovako (obratite pažnju na obeležavanje bojama); ko želi konkretne komentare može pogledati izvorni kod hotspot (1.8), fajl path/hotspot/src/share/vm/oops/markOop.hpp, linija 30.

Sa ovim osnovnim informacijama, sledeće treba razjasniti kako se menja informacija o bravi u MarkWord-u.
Samo gledati gornju sliku je veoma apstraktno; mi programeri najviše volimo da kod govori. Zvanični sajt openjdk nudi alatku za pregled rasporeda objekta u memoriji, JOL (java object layout), koju ćemo uvesti u projekat preko Maven-a.
Maven Package
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.14</version>
</dependency>Sada ćemo kroz kod produbiti razumevanje biased brave.
Scenario 1
public static void main(String[] args) {
Object o = new Object();
log.info("Pre ulaska u sinhronizovani blok, MarkWord je:");
log.info(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
log.info(("Ulazak u sinhronizovani blok, MarkWord je:"));
log.info(ClassLayout.parseInstance(o).toPrintable());
}
}Pogledajmo izlaz:

Gornji primer koristi JOL verziju 0.14; sada ćemo upotrebiti verziju 0.16, jer daje prijateljskije opise. Isti kod, pogledajmo izlaz:

Vidivši ovaj rezultat, neki će se zapitati: nakon JDK 1.6 biased brava je podrazumevano uključena, zašto je inicijalni kod u stanju bez brave, a po ulasku u sinhronizovani blok uz takmičenje biased brava zaobilazi i prebacuje se direktno u laku bravu?
Iako je biased brava podrazumevano uključena, njeno uključivanje ima kašnjenje od oko 4 s. Razlog je što JVM interno na mnogim mestima koristi synchronized; ako se biased brava uključi odmah, pri takmičenju dolazi do nadgradnje brave, što donosi dodatni gubitak performansi, pa postoji strategija kašnjenja.

Kašnjenje se parametrom -XX:BiasedLockingStartupDelay=0 može postaviti na 0, ali se ne preporučuje.
Scenario 2
Hajde da u kodu sačekamo 5 sekundi pre kreiranja objekta i vidimo da li biased brava stupi na snagu.
public static void main(String[] args) throws InterruptedException {
// spavanje 5 s
Thread.sleep(5000);
Object o = new Object();
log.info("Pre ulaska u sinhronizovani blok, MarkWord je:");
log.info(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
log.info(("Ulazak u sinhronizovani blok, MarkWord je:"));
log.info(ClassLayout.parseInstance(o).toPrintable());
}
}Pogledajmo novi rezultat:

Takav rezultat odgovara našem očekivanju, ali stanje biasable ne postoji u tabeli MarkWord-a; zapravo je u pitanju stanje anonimne biased brave — JVM ga postavlja za nas tokom inicijalizacije objekta. Tako, kad nit uđe u sinhronizovani blok:
- Stanje koje može biti biased: direktno CAS-om zameni ThreadID; ako uspe, dobija biased bravu
- Stanje koje ne može biti biased: prelazi u laku bravu
Sada novo pitanje: objekat brave ima konkretnu nit kojoj je „nagnut"; ako dođe nova nit koja izvršava sinhronizovani blok, hoće li biti nagnuta novoj niti?
Scenario 3
public static void main(String[] args) throws InterruptedException {
// spavanje 5 s
Thread.sleep(5000);
Object o = new Object();
log.info("Pre ulaska u sinhronizovani blok, MarkWord je:");
log.info(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
log.info(("Ulazak u sinhronizovani blok, MarkWord je:"));
log.info(ClassLayout.parseInstance(o).toPrintable());
}
Thread t2 = new Thread(() -> {
synchronized (o) {
log.info("Nova nit dobija bravu, MarkWord je:");
log.info(ClassLayout.parseInstance(o).toPrintable());
}
});
t2.start();
t2.join();
log.info("Glavna nit ponovo pregleda objekat brave, MarkWord je:");
log.info(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
log.info(("Glavna nit ponovo ulazi u sinhronizovani blok, MarkWord je:"));
log.info(ClassLayout.parseInstance(o).toPrintable());
}
}Pogledajmo rezultat — dogodila se čudna stvar:

Oznaka 1: inicijalno stanje koje može biti biasedOznaka 2: nakon što je nagnuta glavnoj niti, glavna nit izlazi iz sinhronizovanog blokaOznaka 3: nova nit ulazi u sinhronizovani blok i nadograđuje se u laku bravuOznaka 4: laka brava nove niti izlazi iz sinhronizovanog bloka; glavna nit pregledava i stanje postaje ne-biasableOznaka 5: pošto objekat ne može biti biased, kao u scenariju 1, glavna nit pri ponovnom ulasku u sinhronizovani blok prirodno koristi laku bravu
Time se scenariji jedan, dva i tri mogu sumirati jednom slikom:

Prema ovom rezultatu, biased brava deluje kao „jednokratan posao" — jednom kad se nagne nekoj niti, svaki sledeći pokušaj druge niti da dobije bravu prelazi u laku bravu; takva „naklonost" je veoma ograničena. Zapravo nije tako. Ako pažljivo pogledate oznaku 2 (biased stanje), postoji još jedan epoch koga nismo spominjali — ta vrednost je ključ kojim se prevazilazi to ograničenje. Pre nego što razumemo epoch, moramo upoznati još jedan pojam — opoziv biased brave (epoch će biti detaljno objašnjen kasnije uz grupni opoziv).
Opoziv biased brave
Pre nego što objasnimo opoziv biased brave, moramo razjasniti jedan pojam — opoziv biased brave i puštanje biased brave nisu ista stvar:
- Opoziv: grubo rečeno, takmičenje više niti dovodi do toga da biased režim više ne može da se koristi; uglavnom se naznačava da taj objekat brave više ne može da koristi biased režim
- Puštanje: kao i uobičajeno shvatanje, odnosi se na izlazak iz synchronized metode ili kraj synchronized bloka
Šta je opoziv biased brave?
Povratak iz biased stanja u prethodno stanje, odnosno promena vrednosti 3. bita MarkWord-a (da li je biased)
iz 1 nazad u 0
Ako samo jedna nit dobija bravu, uz „naklonost" prema njoj, nema razloga za opoziv; zato se opoziv biased brave može dogoditi samo u prisustvu takmičenja.
Da bi se opozvala biased brava, a da se pritom ne utiče na nit koja je drži, mora se sačekati da nit koja drži biased bravu stigne do safepoint sigurne tačke (ova sigurna tačka je stanje koje JVM uvodi da bi se osiguralo da se referentni odnosi ne menjaju tokom garbage collection-a; u tom stanju se obustavljaju sve niti). Na toj sigurnoj tački se suspenduje nit koja je dobila biased bravu; detaljnije će biti reči kasnije uz JVM.
Na toj sigurnoj tački nit se možda i dalje nalazi u različitim stanjima; prvo rezime (jer je izvorni kod upravo tako napisan):
- Nit nije živa ili je živa nit koja je izašla iz sinhronizovanog bloka — jednostavno se opoziva biased brava
- Živa nit koja je još uvek unutar sinhronizovanog bloka — tada se prelazi u laku bravu
Sa epoch-om i dalje nema veze, ali to još uvek nisu svi scenariji.
Biased brava je rešenje za poboljšanje efikasnosti u specifičnim scenarijima, što ne znači da svi programi zadovoljavaju te uslove. Na primer, ovi scenariji (uz uključenu biased bravu):
- Jedna nit kreira mnogo objekata i izvrši inicijalne sinhronizovane operacije, a zatim druga nit koristi te objekte kao brave za naredne operacije. Takav slučaj dovodi do velikog broja opoziva biased brave.
- Kada se unapred zna da postoji takmičenje više niti (red proizvođač/potrošač), korišćenje biased brave takođe dovodi do raznih opoziva.
Očigledno, oba scenarija vode opozivu biased brave; jedan opoziv nije problem, ali opozivi u velikom broju ne mogu se zanemariti. Šta onda?
Ne želimo ni da zabranimo biased bravu ni da trpimo troškove masovnih opoziva. Rešenje je konstruisati stepenastu granicu.
Grupni ponovni bias (bulk rebias)
Ovo je brzo rešenje za prvi scenario; po class-i se za svaku class-u održava brojač opoziva biased brave; kad objekat te class-e doživi opoziv biased brave, brojač se uveća za 1; kad dostigne prag za ponovni bias (podrazumevano 20):
BiasedLockingBulkRebiasThreshold = 20JVM smatra da biased brava te class-e ima problem, pa izvršava grupni ponovni bias, čija realizacija koristi gore pomenuti epoch.
Epoch, kao što ime „epoha" kaže, jeste vremenska oznaka. Svaki class objekat ima odgovarajuće polje epoch, a i svaki objekat u stanju biased brave ima to polje u mark word-u; početna vrednost jednaka je vrednosti epoch iz class-e u trenutku kreiranja (tada su te dve vrednosti jednake).
Pri svakom grupnom ponovnom bias-u ta vrednost se uveća za 1 i istovremeno se obilaze stekovi svih niti u JVM-u:
- Pronađu se svi objekti te class-e koji su trenutno u stanju zaključanosti pod biased bravom i njihovo polje
epochpostavi na novu vrednost - Objekti te class-e biased brave koji nisu u stanju zaključanosti (niti ih drži bilo koja nit, ali su ih ranije držale — takvi objekti sigurno imaju biased markword) zadržavaju istu vrednost
epoch
Tako sledeći put, kad se dobija brava, vidi se da epoch trenutnog objekta i class-e epoch nisu isti; po principu sadašnjost ne pita prošlost (prethodna epoha), pa čak i ako je objekat već biased drugoj niti, ne vrši se opoziv, već se CAS-om menja ID niti u markword-u na ID trenutne niti — to je određena optimizacija, jer barem ne dolazi do nadgradnje brave.
Ako su epoch vrednosti iste, nema grupnog ponovnog bias-a; ako markword ima ID niti i pojavi se druga brava u takmičenju, brava se naravno nadograđuje (kao u prethodnom primeru epoch=0).
Grupni ponovni bias je prva stepenasta granica; postoji i druga stepenasta granica.
Grupni opoziv (bulk revoke)
Kad se dostigne prag za ponovni bias, pretpostavimo da brojač te class-e nastavi da raste; kad dostigne prag za grupni opoziv (podrazumevano 40),
BiasedLockingBulkRevokeThreshold = 40JVM smatra da scenarij upotrebe te class-e sadrži takmičenje više niti i označava tu class-u kao ne-biasable. Nakon toga, brave te class-e idu direktno kroz logiku lake brave.
To je druga stepenasta granica; ali tokom prelaska iz prve u drugu, pre potpunog isključivanja biased brave, daje se još jedna šansa za popravku — još jedan tajmer:
BiasedLockingDecayTime = 25000- Ako u roku od 25 sekundi od poslednjeg grupnog ponovnog bias-a, a kumulativni broj opoziva dostigne 40, dolazi do grupnog opoziva (biased brava je definitivno završila igru)
- Ako je prošlo više od 25 sekundi od poslednjeg grupnog ponovnog bias-a, brojač u opsegu
[20, 40)se resetuje i daje se još jedna šansa
Zainteresovani mogu napisati kod za ispitivanje graničnih vrednosti i posmatrati promene u markword-u objekta brave. Time se čitav tok rada biased brave može prikazati jednom slikom:

Ovde biste trebali steći osnovno razumevanje biased brave.
Biased brava i HashCode
U gorešnjem scenariju jedan, stanje bez brave, zaglavlje objekta nema hashcode; ni u stanju biased brave zaglavlje objekta nema hashcode — gde je onda naš hashcode?
Prvo treba znati da hashcode ne upisuje u zaglavlje objekta već pri kreiranju, već tek nakon prvog poziva Object::hashCode() ili System::identityHashCode(Object).
Pošto se hashcode generiše prvi put, ta vrednost bi trebalo da ostane nepromenjena, ali biased brava stalno menja markword objekta brave i sigurno utiče na generisanje hashcode-a. Šta onda? Proverimo kodom:
Scenario 1
public static void main(String[] args) throws InterruptedException {
// spavanje 5 s
Thread.sleep(5000);
Object o = new Object();
log.info("Pre generisanja hashcode-a, MarkWord je:");
log.info(ClassLayout.parseInstance(o).toPrintable());
o.hashCode();
log.info("Nakon generisanja hashcode-a, MarkWord je:");
log.info(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
log.info(("Ulazak u sinhronizovani blok, MarkWord je:"));
log.info(ClassLayout.parseInstance(o).toPrintable());
}
}Pogledajmo rezultat:

Zaključak je: čak i za objekat inicijalizovan u biasable stanju, jednom kad se pozove Object::hashCode() ili System::identityHashCode(Object), pri ulasku u sinhronizovani blok koristiće se direktno laka brava.
Scenario 2
Ako je objekat već biased jednoj niti, a zatim se generiše hashcode, pa zatim ista nit ponovo uđe u sinhronizovani blok — šta će se dogoditi? Pogledajmo kod:
public static void main(String[] args) throws InterruptedException {
// spavanje 5 s
Thread.sleep(5000);
Object o = new Object();
log.info("Pre generisanja hashcode-a, MarkWord je:");
log.info(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
log.info(("Ulazak u sinhronizovani blok, MarkWord je:"));
log.info(ClassLayout.parseInstance(o).toPrintable());
}
o.hashCode();
log.info("Generiše se hashcode");
synchronized (o){
log.info(("Ista nit ponovo ulazi u sinhronizovani blok, MarkWord je:"));
log.info(ClassLayout.parseInstance(o).toPrintable());
}
}Pogledajmo rezultat:

Zaključak je: kao u scenariju jedan, koristi se direktno laka brava.
Scenario 3
Ako je objekat u biased stanju, a unutar sinhronizovanog bloka se pozovu te dve metode — šta će se dogoditi? Nastavimo sa proverom kodom:
public static void main(String[] args) throws InterruptedException {
// spavanje 5 s
Thread.sleep(5000);
Object o = new Object();
log.info("Pre generisanja hashcode-a, MarkWord je:");
log.info(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
log.info(("Ulazak u sinhronizovani blok, MarkWord je:"));
log.info(ClassLayout.parseInstance(o).toPrintable());
o.hashCode();
log.info("U biased stanju, nakon generisanja hashcode-a, MarkWord je:");
log.info(ClassLayout.parseInstance(o).toPrintable());
}
}Pogledajmo rezultat:

Zaključak je: ako je objekat u biased stanju, nakon generisanja hashcode-a prelazi direktno u tešku bravu.
Na kraju, citatom iz knjige opisujmo odnos između brave i hashcode-a.

Teška brava i Object.wait
Object pored gore navedenih metoda hashCode, ima i metod wait(), koji takođe često koristimo u sinhronizovanim blokovima. Kakav uticaj na bravu ima poziv wait? Pogledajmo kod:
public static void main(String[] args) throws InterruptedException {
// spavanje 5 s
Thread.sleep(5000);
Object o = new Object();
log.info("Pre generisanja hashcode-a, MarkWord je:");
log.info(ClassLayout.parseInstance(o).toPrintable());
synchronized (o) {
log.info(("Ulazak u sinhronizovani blok, MarkWord je:"));
log.info(ClassLayout.parseInstance(o).toPrintable());
log.info("wait 2s");
o.wait(2000);
log.info(("Nakon poziva wait, MarkWord je:"));
log.info(ClassLayout.parseInstance(o).toPrintable());
}
}Pogledajmo rezultat:

Zaključak je: metod wait je jedinstven za mutex (tešku bravu); jednom pozvan, prelazi se u tešku bravu (ovo je dobra poenta za intervju).
Na kraju, još malo obogatićemo sliku promena objekta brave:

Zbogom, biased bravo
Ovaj podnaslov vas možda malo uznemiri — zašto oproštamo od biased brave? Zato što su troškovi održavanja postali previsoki. Pogledajte zvaničnu izjavu Open JDK-a, JEP 374: Deprecate and Disable Biased Locking

Vreme ažuriranja te izjave je veoma blizu sadašnjosti; počelo je već u JDK 15.

Jednom rečenicom: troškovi održavanja su previsoki.


Konačno, pre JDK 15 biased brava je podrazumevano enabled, a od JDK 15 podrazumevano je disabled, osim ako se eksplicitno ne uključi preko UseBiasedLocking.
U članku na quarkus-u stvar je opisana još direktnije.

Biased brava je unela ogromnu složenost u JVM; samo mali broj jako iskusnih programera razume čitav proces; održavanje je skupo i znatno koči razvoj novih osobina (s druge strane, ako je savladate, zar ne pripadate tom malom broju iskusnih? Ha ha).
Zaključak
Biased brava je možda ovako završila svoj život; neki direktno pitaju: pošto je deprecated i JDK je već 17, zašto toliko o njoj?
- „Java izlazi kako izlazi, ja koristim Javu 8" — to je stanje mnogih glavnih korisnika; barem verzija koju koriste nije deprecated.
- Na intervjuima se i dalje često pita.
- Ako jednog dana postoji bolji dizajn, „biased brava" se možda vrati u novom obliku; razumevanje promena pomaže boljem shvatanju ideja koje stoje iza dizajna.
- Princip Okamove oštrice — i optimizacije u stvarnosti su iste: ako nije potrebno, ne treba dodavati entitet; ako dodato donosi velike troškove, hrabro ga uklonite i prihvatite malo pada.
Urednik: Chenmo Wang Er; sadržaj pre uređivanja uglavnom potiče iz ovog članka na Zhihuu autora Ri Gong Yi Bing: https://zhuanlan.zhihu.com/p/451061367
