Synchronization
Synchronized, zárak, ReentrantLock és ReadWriteLock
A Java synchronizáció célja, hogy a megosztott, módosítható állapot concurrent hozzáférés mellett is helyes maradjon. Az erős interjúválasz nem csak a kölcsönös kizárást említi, hanem a visibilityt, a happens-before kapcsolatot, a condition waitinget és azt is, mikor jobb választás egy magasabb szintű lock API.
1. Definíció
Mi a synchronizáció?
A synchronizáció azoknak a technikáknak az összessége, amelyek helyessé teszik a párhuzamos hozzáférést.
Ez több annál, hogy „egyszerre csak egy thread mehet be”.
Ide tartozik még:
- a write-ok láthatósága
- az összetett műveletek atomikussága
- az invariánsok megőrzése
- a helyes várakozás és értesítés
- a Java Memory Model szerinti rendezett viselkedés
Miért fontos ez?
Synchronizáció nélkül a threadek láthatnak:
- elavult értékeket
- félbemaradt frissítéseket
- több mezőre kiterjedő törött invariánsokat
- sérült megosztott állapotot
A jól szinkronizált programban az olvasók olyan állapotot látnak, amely egy érvényes végrehajtási sorrendből származhat.
Mit mondjon egy erős válasz?
Egy jó válasz név szerint említi:
synchronized- monitor
- happens-before
wait()/notifyAll()- reentrancy
ReentrantLockReadWriteLock- visibility versus atomicity
2. Alapfogalmak
2.1 Intrinsic locking `synchronized` segítségével
A synchronized kulcsszó intrinsic lockingot ad.
A synchronized blokkba belépve a thread megszerez egy monitort.
Kilépéskor a monitor felszabadul, kivétel esetén is.
Ez ad:
- mutual exclusiont
- memory visibility hatást
Egy sikeres monitor release happens-before kapcsolatban áll ugyanannak a monitornak egy későbbi sikeres acquisitionjével.
Ezért oldja meg a synchronized egyszerre az atomicity és visibility kérdését az őrzött állapotra.
2.1.1 Kifejezések és szerződések, amelyeket itt érdemes kimondani
Ezek a téma teherhordó fogalmai:
synchronized— intrinsic locking kulcsszó- monitor — objecthez kötött lock
- intrinsic lock — beépített, monitoralapú lock
- reentrant lock behavior — ugyanaz a thread újra megszerezheti ugyanazt a lockot
- happens-before — visibility és ordering kapcsolat a Java Memory Modelben
wait()— monitor elengedése és felfüggesztés, amíg a condition változhatnotify()— egy várakozó felébresztésenotifyAll()— az összes várakozó felébresztése- spurious wakeup — jogszerű ébredés úgy is, hogy a várt condition nem teljesült
ReentrantLock— explicit lock API több kontrollalCondition— explicit lockhoz kötött condition queueReadWriteLock— reader és writer koordinációt szétválasztó lock
Ha ezeket pontosan nevezed meg, sokkal tisztább lesz a magyarázat.
2.2 Reentrancy és lock scope
Az intrinsic lock reentrant.
Ugyanaz a thread többször is beléphet ugyanabba a monitorba anélkül, hogy önmagát deadlockolná.
Minden object lehet monitor.
De nem minden objectnek kell annak lennie.
A this, boxed értékek vagy string literálok lockként való használata kockázatos, mert idegen kód is szinkronizálhat rajtuk.
Gyakran biztonságosabb egy private final lock object.
2.3 `wait`, `notify` és `notifyAll`
Ezeket a metódusokat csak a monitor birtokában lehet hívni.
A wait() atomi módon:
- elengedi a monitort
- felfüggeszti a threadet
A thread újraindulhat például:
- értesítés miatt
- interrupt miatt
- timeout miatt
- spurious wakeup miatt
Ezért kell a várakozást ciklusban végezni.
Soha ne feltételezd, hogy egy ébredés biztosan azt jelenti, hogy a condition már igaz.
2.4 Explicit lockok
A ReentrantLock olyan képességeket ad, amelyeket az intrinsic locking nem:
tryLock()- időzített acquisition
- interruptible acquisition
- opcionális fairness
- több
Conditionqueue
A ReadWriteLock lehetővé teszi az egyidejű olvasókat és az exkluzív írót.
Csak akkor segít, ha a workload valóban indokolja.
3. Gyakorlati használat
Mikor a legjobb a `synchronized`?
A synchronized jó választás, ha:
- kicsi és lokális a lock scope
- könnyen leírható az invariáns
- a readability fontosabb, mint a speciális lock feature-ök
- monitoralapú condition queue kell
Sok kis object graph védelmére ez a legtisztább megoldás.
Mikor indokolt az explicit lock?
A ReentrantLock akkor indokolt, ha kell:
- timeout alapú deadlock-elkerülés
- interruptálható blokkolás
- több várakozási condition
- fairness konfiguráció
Az előnye a rugalmasság.
A költsége a fegyelem.
Ha elfelejted a finally-ben lévő unlock()-ot, correctness bugot hozol létre.
`ReadWriteLock` realizmus
A ReadWriteLock jól hangzik skálázási szempontból.
Néha tényleg segít.
De csak akkor, ha:
- sok az olvasás
- az olvasások elég hosszúak ahhoz, hogy megérje a koordinációs költség
- kevesebb az írás
- valódi contention van
Kis kritikus szakaszoknál vagy write-heavy workloadnál lassabb is lehet, mint egy sima mutex.
4. Kód példák
1. példa: egyszerű intrinsic lock
class Counter {
private final Object lock = new Object();
private int value;
int incrementAndGet() {
synchronized (lock) {
value++;
return value;
}
}
int get() {
synchronized (lock) {
return value;
}
}
}
Kulcspontok:
- az olvasás és írás is őrzött
- egy lock egy invariáns scope-ot véd
- a visibility ugyanebből a monitor-protokollból jön
2. példa: helyes wait loop
class BoundedBuffer<T> {
private final Queue<T> queue = new ArrayDeque<>();
private final int capacity;
BoundedBuffer(int capacity) {
this.capacity = capacity;
}
public synchronized void put(T item) throws InterruptedException {
while (queue.size() == capacity) {
wait();
}
queue.add(item);
notifyAll();
}
public synchronized T take() throws InterruptedException {
while (queue.isEmpty()) {
wait();
}
T item = queue.remove();
notifyAll();
return item;
}
}
Kulcspontok:
while, nemif- a
wait()condition-alapú várakozás - a
notifyAll()többféle várakozó esetén biztonságosabb
3. példa: explicit lock `tryLock`-kal
class SafeTransfer {
private final ReentrantLock lock = new ReentrantLock();
private int balance;
boolean tryWithdraw(int amount) throws InterruptedException {
if (!lock.tryLock(100, TimeUnit.MILLISECONDS)) {
return false;
}
try {
if (balance < amount) {
return false;
}
balance -= amount;
return true;
} finally {
lock.unlock();
}
}
}
Kulcspontok:
- a
tryLocktimeout-alapú koordinációt támogat - az
unlock()-nakfinallyblokkban kell lennie
5. Trade-offok
| Döntés | Előny | Költség vagy kockázat |
|---|---|---|
synchronized |
Egyszerű, olvasható, automatikusan elenged | Kevesebb vezérlési lehetőség |
wait/notifyAll |
Beépített condition koordináció | Könnyű rosszul használni ciklus nélkül |
ReentrantLock |
Időzített és interruptálható acquisition, több condition | Manuális unlock fegyelem kell |
ReadWriteLock |
Segíthet read-heavy contention esetén | Gyakran túlhasznált vagy lassabb a gyakorlatban |
Gyakorlati trade-off elemzés
A synchronizáció nem egyetlen eszköz.
Inkább egy kontroll-létra.
A nagyobb kontroll rendszerint együtt jár:
- nagyobb kifejezőerővel
- több komplexitással
- több hibalehetőséggel
Ezért az alapértelmezett választás többnyire a legegyszerűbb helyes eszköz legyen.
6. Gyakori hibák
1. hiba: azt hinni, hogy a synchronizáció csak arról szól, hogy egyszerre egy thread mehet be
A visibility is ugyanilyen fontos.
Helyes megközelítés:
- a mutual exclusiont és a happens-before kapcsolatot együtt magyarázd
2. hiba: `if` használata `while` helyett várakozáskor
A spurious wakeup és a versenyző fogyasztók tönkreteszik ezt a logikát.
Helyes megközelítés:
- a conditiont mindig ciklusban ellenőrizd újra
3. hiba: nyilvánosan elérhető objectekre lockolás
Ez véletlen contentionhöz vezethet idegen kóddal.
Helyes megközelítés:
- használj private final lock objectet
4. hiba: elmaradó `unlock()` explicit locknál
Ez deadlockolhatja a komponenst.
Helyes megközelítés:
- mindig
finallyblokkban engedd el a lockot
5. hiba: azt gondolni, hogy a `ReadWriteLock` automatikusan gyorsabb
Ez workloadfüggő.
Helyes megközelítés:
- valódi contention mellett mérj, mielőtt ezt választod
6. hiba: több, egymástól eltérő condition keverése ugyanazon a monitoron
Ez felesleges wakeupokhoz és zavaros működéshez vezet.
Helyes megközelítés:
- ha a condition logika bonyolult, fontold meg az explicit
Conditionqueue-kat
7. Mélymerülés
7.1 Visibility versus atomicity
A shared field probléma nem mindig csak atomicity kérdés.
Lehet, hogy minden egyes write „egyszerű”, mégis stale state-et látnak az olvasók.
Ezért a synchronizációt mindig memory visibility oldalról is tárgyalni kell.
7.2 A condition waiting valójában protokolltervezés
A wait() és notifyAll() nem pusztán API hívások.
Egy protokoll részei:
- mi a védett condition?
- melyik lock őrzi?
- ki változtatja?
- ki vár rá?
- ki jelez a state transition után?
Ha ez a protokoll homályos, concurrency bugok jönnek.
7.3 `ReentrantLock` versus intrinsic monitor
Az intrinsic locking gyakran elég.
A ReentrantLock akkor indokolt, amikor maga a lock policy is a design része.
Például:
- interruption-aware waiting
- időzített acquisition
- több condition queue
- szabályozott fairness
7.4 Read-heavy minták
Néha a legjobb válasz nem a ReadWriteLock.
Néha inkább ez:
- immutable snapshot csere
ConcurrentHashMap- a shared state csökkentése
A senior válasz ezeket az alternatívákat összehasonlítja.
8. Interjúkérdések
1. Mit garantál a `synchronized`?
Mutual exclusiont és memory visibility kapcsolatot a monitor release és acquisition révén.
2. Miért kell a `wait()`-ot ciklusban használni?
Mert a spurious wakeup legális, és a conditiont újra kell ellenőrizni.
3. Mi az a reentrancy?
Ugyanaz a thread újra megszerezheti ugyanazt a lockot, amelyet már tart.
4. Miért kockázatos néha a `this`-re lockolni?
Mert külső kód is szinkronizálhat ugyanarra az objectre.
5. Mit csinál a `notifyAll()`?
Felébreszti a monitoron várakozó összes threadet.
6. Mikor jobb a `ReentrantLock`, mint a `synchronized`?
Ha időzített, interruptálható vagy több conditionös lock viselkedés kell.
7. A `ReadWriteLock` mindig gyorsabb?
Nem.
Az előnye teljesen workloadfüggő.
8. Mi az a happens-before kapcsolat ebben a kontextusban?
Olyan visibility és ordering kapcsolat, amelyet synchronizációs műveletek hoznak létre.
9. Miért kötelező az `unlock()` `finally` blokkban?
Mert kivétel esetén is garantálni kell a lock release-t.
10. Mi a senior szintű tanulság?
A synchronizáció legalább annyira protokolltervezés, mint kulcsszóhasználat.
9. Szószedet
| Fogalom | Jelentés |
|---|---|
synchronized |
Intrinsic locking konstrukció |
| monitor | Egy objecthez kötött lock |
| happens-before | Visibility és ordering kapcsolat |
| reentrant | Ugyanaz a thread újra megszerezheti ugyanazt a lockot |
wait() |
Monitor elengedése és felfüggesztés |
notifyAll() |
Minden várakozó felébresztése |
| spurious wakeup | Ébredés anélkül, hogy a desired condition tényleg igaz lenne |
ReentrantLock |
Több kontrollt adó explicit lock |
Condition |
Explicit lockhoz kötött condition queue |
ReadWriteLock |
Reader/writer koordinációs lock |
10. Gyorsreferencia
- a
synchronizedmutual exclusiont és visibilityt ad - egy jól definiált lock őrizzen egy invariáns scope-ot
- várakozáskor
while, neif - a
wait()várakozás közben elengedi a monitort - a
notifyAll()gyakran biztonságosabb, mint anotify() - a
ReentrantLockidőzített és interruptálható acquisitiont is tud - az
unlock()mindigfinallyblokkban legyen - a
ReadWriteLockcsak a megfelelő workload mellett segít - a private lock object gyakran jobb, mint a publikus lock target
- interjún nevezd meg a happens-before, reentrancy és spurious wakeup fogalmakat
🎮 Játékok
10 kérdés