KözéphaladóOlvasási idő: ~10 perc

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
  • ReentrantLock
  • ReadWriteLock
  • 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áltozhat
  • notify() — egy várakozó felébresztése
  • notifyAll() — 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 kontrollal
  • Condition — explicit lockhoz kötött condition queue
  • ReadWriteLock — 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 Condition queue

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, nem if
  • 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 tryLock timeout-alapú koordinációt támogat
  • az unlock()-nak finally blokkban 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 finally blokkban 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 Condition queue-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 synchronized mutual exclusiont és visibilityt ad
  • egy jól definiált lock őrizzen egy invariáns scope-ot
  • várakozáskor while, ne if
  • a wait() várakozás közben elengedi a monitort
  • a notifyAll() gyakran biztonságosabb, mint a notify()
  • a ReentrantLock időzített és interruptálható acquisitiont is tud
  • az unlock() mindig finally blokkban legyen
  • a ReadWriteLock csak 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