Duboko razumevanje Java reentrantne brave ReentrantLock
ReentrantLock, reentrantna brava, je klasa koja implementira Lock interfejs, a i brava koja se vrlo često koristi u stvarnom programiranju. Podržava reentransnost, što znači da može više puta da zaključa deljeni resurs — kada trenutna nit pribavi bravu, može je pribaviti ponovo bez blokiranja.
Da bi podržala reentransnost, moraju da se reše dva problema:
- Kada nit pribavi bravu, ako je nit koja je već pribavila bravu trenutna nit, odmah ponovo uspešno pribavlja.
- Pošto će brava biti pribavljena n puta, samo nakon što bude puštena isti broj n puta, ta brava se smatra potpuno puštenom.
Znamo da se komponente sinhronizacije uglavnom izražavaju sopstvenom sinhronizacionom semantikom kroz prepisivanje nekoliko protected metoda AQS-a.
Analiza izvornog koda ReentrantLock-a
Za prvi problem, hajde da vidimo kako ReentrantLock implementira, na primeru nefer brave i provere da li trenutna nit može da pribavi bravu. Ključna metoda je nonfairTryAcquire unutrašnje klase Sync:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//1. Ako bravu ne drži nijedna nit, trenutna nit može da je pribavi
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//2. Ako je drži, proverava da li je nit koja je drži trenutna nit
else if (current == getExclusiveOwnerThread()) {
// 3. Ponovno pribavljanje, brojač se uvećava za jedan
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}Logika ovog koda je vrlo jednostavna, pogledajte komentare. Da bi se podržala reentransnost, u drugom koraku je dodata logika obrade: ako je bravu već zauzela nit, nastavlja se provera da li je nit koja je zauzela trenutna nit. Ako jeste, stanje sinhronizacije se uvećava za 1 i vraća true, što znači da može ponovo uspešno da pribavi. Pri svakom ponovnom pribavljanju vrši se uvećanje stanja sinhronizacije za jedan. Koja je onda ideja obrade pri puštanju? (I dalje na primeru nefer brave) Ključna metoda je tryRelease:
protected final boolean tryRelease(int releases) {
//1. Stanje sinhronizacije se smanjuje za 1
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
//2. Tek kada je stanje sinhronizacije 0, brava je uspešno puštena i vraća true
free = true;
setExclusiveOwnerThread(null);
}
// 3. Brava nije potpuno puštena, vraća false
setState(c);
return free;
}Logiku koda pogledajte u komentarima. Treba napomenuti da puštanje reentrantne brave mora da sačeka da stanje sinhronizacije bude 0 da bi se brava smatrala uspešno puštenom, inače brava nije puštena. Ako je brava pribavljena n puta, a puštena n-1 puta, brava nije potpuno puštena i vraća false; tek kada je puštena n puta smatra se uspešno puštenom i vraća true. Do sada možemo da razjasnimo implementaciju reentransnosti ReentrantLock-a, odnosno da razumemo prvi uslov sinhronizacione semantike.
ReentrantLock podržava dve vrste brava: fer bravu i nefer bravu. Šta je fer? Odnosi se na pribavljanje brave; ako je brava fer, redosled pribavljanja brave treba da odgovara apsolutnom vremenskom redosledu zahteva, odnosno da ispunjava FIFO. Konstruktor ReentrantLock-a bez argumenata kreira nefer bravu; izvorni kod je:
public ReentrantLock() {
sync = new NonfairSync();
}Pored toga pruža se i drugi način — može se proslediti boolean vrednost: true znači fer bravu, false znači nefer bravu; izvorni kod je:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}Pri pribavljanju nefer brave (metoda nonfairTryAcquire), samo se jednostavno dobija trenutno stanje i vrši neka logička obrada, bez uzimanja u obzir stanja čekanja niti u trenutnom redu za sinhronizaciju.
Hajde da vidimo kakva je logika obrade fer brave; ključna metoda je:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}Logika ovog koda je u suštini ista kao kod nonfairTryAcquire; jedina razlika je u tome što je dodata logička provera hasQueuedPredecessors. Iz imena metode se može znati da ona služi za proveru da li trenutni čvor u redu za sinhronizaciju ima prethodni čvor. Ako ima prethodni čvor, to znači da je neka nit ranije zatražila resurs od trenutne niti. Prema fer principu, trenutna nit ne uspeva da zatraži resurs. Tek ako trenutni čvor nema prethodni čvor, postoji potreba za daljom logičkom proverom.
Fer brava svaki put pribavlja bravu iz prvog čvora u redu za sinhronizaciju, dok nefer brava ne mora — moguće je da nit koja je upravo pustila bravu ponovo pribavi bravu.
Upotreba ReentrantLock-a
Način upotrebe ReentrantLock-a je sličan ključnoj reči synchronized — sinhronizacija se ostvaruje zaključavanjem i puštanjem brave. Hajde da vidimo način upotrebe ReentrantLock-a, na primeru nefer brave:
public class ReentrantLockTest {
private static final ReentrantLock lock = new ReentrantLock();
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count);
}
}Kod je vrlo jednostavan: dve niti vrše po 10000 uvećanja promenljive count, a zatim se ispisuje vrednost count. Hajde da vidimo rezultat:
20000Vidi se da su dve niti izvršile 20000 uvećanja promenljive count, što pokazuje da ReentrantLock podržava reentransnost. Hajde da vidimo i način upotrebe fer brave — dovoljno je promeniti konstruktor ReentrantLock-a u fer bravu:
private static final ReentrantLock lock = new ReentrantLock(true);Rezultat je:
20000Vidi se da je rezultat fer brave isti kao rezultat nefer brave, jer je način implementacije fer brace u suštini isti kao način implementacije nefer brave, samo što je pri pribavljanju brave dodata logička provera da li trenutni čvor ima prethodni čvor.
- Fer brava: pribavlja bravu po redosledu zahteva niti, odnosno prvi-došao-prvi-dobija.
- Nefer brava: redosled pribavljanja brave može se razlikovati od redosleda zahteva, što može dovesti do toga da neke niti brže pribave bravu.
Treba napomenuti da pri upotrebi ReentrantLock-a brava mora biti pribavljena pre početka try bloka, i da pre zaključavanja ne sme biti bačen izuzetak, jer bi se u suprotnom u bloku finally brave mogla pustiti (brava ReentrantLock-a mora ručno da se pusti u finally).
Netačan ❎ primer:
Lock lock = new XxxLock();
// ...
try {
// Ako se ovde baci izuzetak, direktno se izvršava kod bloka finally
doSomething();
// Bez obzira na to da li je brava uspešno pribavljena, blok finally će se izvršiti
lock.lock();
doOthers();
} finally {
lock.unlock();
}Tačan ✅ primer:
Lock lock = new XxxLock();
// ...
lock.lock();
try {
doSomething();
doOthers();
} finally {
lock.unlock();
}ReentrantLock i synchronized
I ReentrantLock i ključna reč synchronized služe za implementaciju sinhronizacije. Koja je razlika između njih? Hajde da vidimo njihovo poređenje:
- ReentrantLock je klasa, dok je synchronized ključna reč u Javi.
- ReentrantLock može da implementira višestruko izbornu obaveštenja (može da veže više Condition-a), dok synchronized može samo putem metoda wait i notify/notifyAll da probudi jednu nit ili probudi sve niti (jednostruko obaveštenje).
- ReentrantLock mora ručno da pusti bravu. Obično je potrebno u bloku finally pozvati metodu unlock kako bi se osiguralo da je brava ispravno puštena.
- synchronized automatski pušta bravu — kada se sinhronizovani blok završi, JVM je automatski pušta, bez ručnog zahvata.
- ReentrantLock: obično pruža bolje performanse, posebno u okruženjima sa visokim stepenom takmičenja.
- synchronized: u nekim situacijama performanse mogu biti nešto slabije, ali sa nadogradnjom JDK verzija razlika u performansama više nije velika.
Sledeći je jednostavan demo upoređivanja performansi:
import java.util.concurrent.locks.ReentrantLock;
public class PerformanceTest {
private static final int NUM_THREADS = 10;
private static final int NUM_INCREMENTS = 1_000_000;
private int count1 = 0;
private int count2 = 0;
private final ReentrantLock lock = new ReentrantLock();
private final Object syncLock = new Object();
public void increment1() {
lock.lock();
try {
count1++;
} finally {
lock.unlock();
}
}
public void increment2() {
synchronized (syncLock) {
count2++;
}
}
public static void main(String[] args) throws InterruptedException {
PerformanceTest test = new PerformanceTest();
// Test ReentrantLock
long startTime = System.nanoTime();
Thread[] threads = new Thread[NUM_THREADS];
for (int i = 0; i < NUM_THREADS; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < NUM_INCREMENTS; j++) {
test.increment1();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
long endTime = System.nanoTime();
System.out.println("ReentrantLock time: " + (endTime - startTime) + " ns");
// Test synchronized
startTime = System.nanoTime();
for (int i = 0; i < NUM_THREADS; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < NUM_INCREMENTS; j++) {
test.increment2();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
endTime = System.nanoTime();
System.out.println("synchronized time: " + (endTime - startTime) + " ns");
}
}Izlazni rezultat:
ReentrantLock time: 269913857 ns
synchronized time: 350595013 nsOvaj test pokušava da izvrši višestruke operacije uvećanja pod oba mehanizma brave, a zatim meri potrebno vreme.
Rezime
Ovaj tekst je uglavnom predstavio princip implementacije ReentrantLock-a i poređenje sa ključnom reči synchronized.
Urednik: Chenmo Wang Er. Sadržaj pre uređivanja uglavnom potiče iz GitHub repozitorijuma CL0610 https://github.com/CL0610/Java-concurrency
