Intervjui s 25-godišnjom studentkinjom — ključna reč synchronized objašnjena do detalja
„Sanme, danas ćemo naučiti ključnu reč synchronized“, rekao sam bacivši pogled na Sanme koja je sedela na sofi — igrala je Honor of Kings.
Sanme (lepa i dopadljiva) otpi čašu ledenog cole-a i reče: „Dobro, Erge. Samo trenutak, da završim ovu partiju, gledaj me kako pravim trostruko ubistvo.“
U Javi ključna reč synchronized može da osigura da u istom trenutku samo jedna nit izvršava određeni metod ili određeni blok koda (pre svega radi operacija nad deljenim podacima u metodu ili bloku). Treba obratiti pažnju i na još jednu važnu ulogu synchronized: on osigurava da izmene jedne niti (pre svega izmene deljenih podataka) budu vidljive drugim nitima (osigurava vidljivost i može potpuno zameniti funkciju volatile).
Ključna reč synchronized se najčešće koristi na sledeća 3 načina:
- sinhronizovani metod, zaključava trenutni objekat (this); pre ulaska u sinhronizovani kod mora se pribaviti brava trenutnog objekta;
- sinhronizovani statički metod, zaključava trenutnu klasu (zaključava se Class objekat); pre ulaska u sinhronizovani kod mora se pribaviti brava trenutne klase;
- sinhronizovani blok koda, navodi se objekat zaključavanja; pre ulaska u sinhronizovani blok mora se pribaviti brava zadatog objekta.
synchronized sinhronizovani metod
Dodavanjem ključne reči synchronized u deklaraciju metoda može se osigurati da u svakom trenutku samo jedna nit izvršava taj metod.
Pogledajmo kod:
public class AccountingSync implements Runnable {
//deljeni resurs (kritični resurs)
static int i = 0;
// synchronized sinhronizovani metod
public synchronized void increase() {
i ++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String args[]) throws InterruptedException {
AccountingSync instance = new AccountingSync();
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("static, i output:" + i);
}
}Izlaz:
/**
* Izlaz:
* static, i output:2000000
*/Ako se ispred metoda increase() ne stavi synchronized, pošto i++ nije atomski, konačni rezultat će biti manji od 2000000; detaljnu analizu možete pogledati u članku o „volatile“.
Napomena: jedan objekat ima samo jednu bravu; kada jedna nit pribavi bravu tog objekta, druge niti ne mogu pribaviti bravu istog objekta, pa ne mogu pristupiti drugim synchronized metodima tog objekta, ali mogu pristupiti drugim ne-synchronized metodama istog objekta.
Međutim, ako nit A treba da pristupi synchronized metodu f1 objekta obj1 (brava trenutnog objekta je obj1), a druga nit B treba da pristupi synchronized metodi f2 objekta obj2 (brava trenutnog objekta je obj2), to je dozvoljeno:
public class AccountingSyncBad implements Runnable {
//deljeni resurs (kritični resurs)
static int i = 0;
// synchronized sinhronizovani metod
public synchronized void increase() {
i ++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String args[]) throws InterruptedException {
// kreiraju se dve nove instance AccountingSyncBad
Thread t1 = new Thread(new AccountingSyncBad());
Thread t2 = new Thread(new AccountingSyncBad());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("static, i output:" + i);
}
}Izlaz:
/**
* Izlaz:
* static, i output:1224617
*/Razlika u odnosu na prethodni kod je u tome što smo kreirali dva objekta AccountingSyncBad, a zatim pokrenuli dve različite niti koje rade nad deljenom promenljivom i; nažalost, rezultat je 1224617 umesto očekivanog 2000000.
Razlog je ozbiljna greška: iako smo synchronizedom sinhronizovali metod increase, kreirali smo dva različita objekta, što znači da postoje dve različite brave objekata. Tako t1 i t2 ulaze u svoje brave objekata — drugim rečima, niti t1 i t2 koriste različite brave, pa nitna bezbednost ne može biti garantovana.
Svaki objekat ima svoju bravu objekta; brave različitih objekata se ne međusobno utiču.
Rešenje ovog problema je da se synchronized primeni na statički metod increase — tada brava objekta postaje brava trenutne klase; pošto klasa uvek postoji samo jedna, bez obzira na to koliko se objekata kreiralo, brava objekta u tom slučaju je jedinstvena.
Pogledajte: Objekti i klase
synchronized sinhronizovani statički metod
Kada synchronized sinhronizuje statički metod, zaključava se Class objekat trenutne klase, koji ne pripada određenom objektu. Pribavljanje brave Class objekta trenutne klase ne utiče na pribavljanje brave instance objekta — ona se međusobno ne ometaju, u suštini je razlika u this i Class.
Pošto statičke promenljive ne pripadaju isključivo jednom objektu, preko Class brave može se kontrolisati konkurentni pristup statičkim promenljivama.
Treba napomenuti da ako nit A pozove ne-statički synchronized metod nekog objekta, a nit B pozove statički synchronized metod klase kojoj taj objekat pripada, neće doći do uzajamnog isključivanja, jer je brava koju zauzima pristup statičkom synchronized metodu Class objekat trenutne klase, dok je brava koju zauzima pristup ne-statičkom synchronized metodu brava trenutnog objekta (this). Pogledajte kod:
public class AccountingSyncClass implements Runnable {
static int i = 0;
/**
* Sinhronizovani statički metod, brava je Class objekat trenutne klase, odnosno
* Class objekat koji odgovara klasi AccountingSyncClass
*/
public static synchronized void increase() {
i++;
}
// ne-statički, pristup s različitim bravama ne dovodi do uzajamnog isključivanja
public synchronized void increase4Obj() {
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
//kreira novu instancu
Thread t1=new Thread(new AccountingSyncClass());
//kreira novu instancu
Thread t2=new Thread(new AccountingSyncClass());
//pokreće niti
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
/**
* Izlaz:
* 2000000
*/Pošto synchronized sinhronizuje statički metod increase, za razliku od sinhronizacije metoda instance, objekat brave je Class objekat trenutne klase.
Obratite pažnju na to da je metod increase4Obj u kodu metod instance, čija je brava objekta trenutna instanca (this); ako ga druga nit pozove, neće doći do uzajamnog isključivanja — jer su objekti brave različiti; u tom slučaju može doći do problema nitne bezbednosti (radi se nad deljenom statičkom promenljivom i).
synchronized sinhronizovani blok koda
U nekim slučajevima metod koji pišemo sadrži mnogo koda, s vremenski zahtevnim operacijama, dok je deo koji treba sinhronizovati samo mali; ako se cela metoda sinhronizuje, to može biti kontraproduktivno. Tada možemo iskoristiti sinhronizovani blok koda kojim obuhvatamo samo deo koji treba sinhronizovati.
Primer:
public class AccountingSync2 implements Runnable {
static AccountingSync2 instance = new AccountingSync2(); // singleton gladna inicijalizacija
static int i=0;
@Override
public void run() {
//ostale vremenski zahtevne operacije se izostavljaju...
//koristi sinhronizovani blok koda za sinhronizovanu operaciju nad promenljivom i, objekat brave je instance
synchronized(instance){
for(int j=0;j<1000000;j++){
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}Izlaz:
/**
* Izlaz:
* 2000000
*/Synchronized smo primenili na zadatu instancu instance — trenutna instanca je objekat brave. Kada nit uđe u blok obuhvaćen synchronized-om, zahteva se da trenutna nit drži bravu instance; ako druga nit već drži tu bravu objekta, nova nit mora čekati. Tako se osigurava da u svakom trenutku samo jedna nit izvršava i++.
Naravno, pored instance možemo koristiti this (trenutnu instancu) ili Class objekat trenutne klase kao bravu:
//this, brava objekta trenutne instance
synchronized(this){
for(int j=0;j<1000000;j++){
i++;
}
}
//brava Class objekta
synchronized(AccountingSync.class){
for(int j=0;j<1000000;j++){
i++;
}
}synchronized i happens-before
Preurednjivanje instrukcija smo spomenuli uz JMM; ovde ćemo ga ponovo razmotriti u vezi sa ključnom reči synchronized.
Pogledajte sledeći kod:
class MonitorExample {
int a = 0;
public synchronized void writer() { //1
a++; //2
} //3
public synchronized void reader() { //4
int i = a; //5
//……
} //6
}Pretpostavimo da nit A izvršava metod writer(), a zatim nit B izvršava metod reader(). Prema pravilima happens-before, relacije happens-before u ovom procesu mogu se podeliti na:
- Prema pravilu programskog reda, 1 happens-before 2, 2 happens-before 3; 4 happens-before 5, 5 happens-before 6.
- Prema pravilu brave monitora, 3 happens-before 4.
- Prema tranzitivnosti happens-before, 2 happens-before 5.
U Java memorijskom modelu pravilo brave monitora je vrsta happens-before pravila koje propisuje da operacija oslobađanja brave monitora (monitor lock) — odnosno brave uzajamnog isključivanja — happens-before pre narednog zaključavanja te iste brave. Jednostavno rečeno, to znači da kada jedna nit oslobodi bravu, a druga nit pribavi istu bravu, sve izmene koje je prva nit načinila pre oslobađanja brave postaju vidljive drugoj niti.
Grafički prikaz gornjih relacija happens-before je:

Na gornjoj slici svaka strelica koja povezuje dva čvora predstavlja jednu relaciju happens-before. Crne strelice označavaju pravilo programskog reda; narandžaste pravilo brave monitora; plave happens-before garanciju koja proizlazi iz kombinacije ovih pravila.
Slika pokazuje da nakon što nit A oslobodi bravu, nit B pribavlja istu bravu. 2 happens-before 5. Dakle, sve deljene promenljive koje su bile vidljive niti A pre oslobađanja brave postaće odmah vidljive niti B nakon što pribavi istu bravu.
Drugim rečima, synchronized sprečava preurednjivanje između koda unutar kritične sekcije i koda van nje; između izvršenja a++ u writer() i čitanja a u reader() postoji relacija happens-before, koja osigurava redosled izvršenja i memorijsku vidljivost.
synchronized je reentrantna brava
Sa stanovišta dizajna brave uzajamnog isključivanja, kada jedna nit pokuša da radi nad kritičnim resursom čiju bravu drži druga nit, ušla bi u blokirano stanje; ali kada ista nit ponovo zatraži kritični resurs čiju bravu već drži, to je reentrantna brava i zahtev će uspeti.
synchronized je reentrantna brava, pa ista nit koja poziva synchronized metod sme unutar tela metode pozvati drugi synchronized metod istog objekta, kao u primeru:
public class AccountingSync implements Runnable{
static AccountingSync instance=new AccountingSync();
static int i=0;
static int j=0;
@Override
public void run() {
for(int j=0;j<1000000;j++){
//this, brava objekta trenutne instance
synchronized(this){
i++;
increase();//reentrantnost synchronized-a
}
}
}
public synchronized void increase(){
j++;
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}Klasa AccountingSync definiše statičku instancu instance i dva statička cela broja i i j; statičke promenljive dele sve instance objekta.
U metodu run koristi se
synchronized(this)za zaključavanje. Objekat brave je this, odnosno instanca AccountingSync. U zaključanom bloku uvećava se statička promenljiva i i poziva metod increase.Metod increase je sinhronizovani metod koji uvećava j. Pošto je i increase sinhronizovan, on može biti pozvan iz metoda run iako je brava već pribavljena — to je reentrantnost ključne reči synchronized.
U metodu main kreiraju se dve niti t1 i t2 koje dele isti Runnable objekat, odnosno istu instancu AccountingSync. Zatim se pokreću obe niti i metodom join čeka da obe završe, pa se ispisuje vrednost i.
U ovom programu i synchronized(this) i synchronized metod koriste isti objekat brave (trenutnu instancu AccountingSync) i vrše uvećanje statičkih promenljivih i i j, pa se u višenitnom okruženju osigurava da su operacije nad i i j nitno bezbedne.
Kratak pregled
„Dobro, Sanme, za danas toliko.“ Jedva sam izgovorio do kraja, uzdahnuo, namestio naočare i rekao Sanme.
Zapamtite tri načina primene synchronized, analizu preurednjivanja instrukcija i reentrantnost synchronized; kroz današnje učenje uglavnom možete savladati upotrebu synchronized.
Sinhronizacija donesi izvesne troškove performansi, pa je treba razumno koristiti. Ne bi trebalo sinhronizovati celu metodu ili veći opseg koda, već što više sužavati opseg sinhronizacije.
U ranim verzijama JVM-a synchronized je bio težak jer blokiranje i buđenje niti zahtevaju uplitanje operativnog sistema. Ali u kasnijim verzijama JVM-a synchronized je znatno optimizovan — uvođenjem bias-brave, lako-brzinske brave i adaptivnog spina — tako da synchronized danas nije nužno težak; u mnogim slučajevima performanse su odlične i može se slobodno koristiti.
Urednik: Chenmo Wang Er, sadržaj pre uređivanja pretežno potiče od Lou Zai-ja, člana Ergovog tehničkog tima Paicoding; originalni link: volatile.
Na GitHub-u je konačno stigao drugi PDF „Mali priručnik o konkurentnom programiranju“open-source baze znanja sa preko 17000 zvezdica „Ergov put ka naprednom Javom“! Obuhvata osnovne pojmove i načine upotrebe niti, memorijski model Java-e, synchronized, volatile, CAS, AQS, ReentrantLock, bazene niti, konkurentne kontejnere, ThreadLocal, model proizvođač-potrošač i druge teme koje su obavezne za intervjue i razvoj — ukupno preko 150 000 reči i preko 200 ručno nacrtanih ilustracija, što se može opisati kao pristupačno i sa humorom... Više detalja: Odlično, Ergov put ka naprednom konkurentnom programiranju.pdf
