How do you choose between synchronized, ReentrantReadWriteLock, and StampedLock for protecting shared state?
answer
- Default to synchronized (simple, reentrant, auto-release)
- ReadWriteLock when reads >> writes AND reads non-trivial
- StampedLock for hot tiny read-mostly; non-reentrant, no Conditions
- Single field → volatile / Atomic* / LongAdder
- Always measure; fancier lock often loses
basics
~20 sUse synchronized by default — it's simplest. Move to ReentrantReadWriteLock when reads greatly outnumber writes and you want readers to run in parallel. Use StampedLock for very hot, short read paths where you can use optimistic reads, but only if you don't need reentrancy or condition waiting.
solid answer
~40 sStart with synchronized (or a plain ReentrantLock): it's the simplest, is reentrant, auto-releases, and is fine for low-to-moderate contention. Choose ReentrantReadWriteLock when the workload is genuinely read-heavy and reads are non-trivial in length, so concurrent readers gain real parallelism; it's reentrant, supports lock downgrading and Conditions, but writers can starve under a read-biased policy. Reach for StampedLock only for very hot, very short read-mostly sections where optimistic reads (no lock, validate after) remove the read-lock CAS overhead — accepting that it is non-reentrant, has no Conditions, and demands careful stamp discipline. Above all, measure: a read/write lock can be slower than synchronized when writes are frequent or sections are tiny. And for single-variable updates, an Atomic class or VarHandle may beat any lock.
go deeper
Knows synchronized is the simple default and that read/write locks let multiple readers run at once.
Can state that ReentrantReadWriteLock suits read-heavy data and StampedLock targets hot read paths, and that you start simple.
Reasons from read/write ratio and section length, knows StampedLock's non-reentrancy/no-Conditions constraints, and considers Atomic/volatile alternatives; insists on measuring.
Owns the decision framework across a system, balances readability and maintainability against measured throughput, and chooses among immutability, lock-free, and the three lock primitives with benchmark evidence.
### The decision is about contention shape, not preference The right synchronization primitive depends on **how** threads contend for the data — the ratio of reads to writes, the length of the critical section, and whether you need features like reentrancy or condition waiting. Here is a principled walk through the options. ### 1. `synchronized` (and plain `ReentrantLock`) — the default `synchronized` is a mutual-exclusion lock: one thread in the critical section at a time. Reasons it is the default: - **Simplest and least error-prone** — automatic release at block exit (no `finally` to forget); the JVM optimizes it well (biased/thin locks historically, lock elision). - **Reentrant** — a holding thread can re-enter. - Correct for **low-to-moderate contention** and **short** sections regardless of read/write mix. `ReentrantLock` adds `tryLock`, timeouts, interruptibility, fairness, and `Condition`s when you need them, at the cost of manual `try/finally`. **Limitation:** all access serializes, even pure reads. ### 2. `ReentrantReadWriteLock` — when reads dominate Splits into a shared read lock and an exclusive write lock: many readers in parallel, or one writer. Choose it when **both** hold: - Reads **greatly outnumber** writes (e.g. a cache, config holder, lookup table), **and** - Read critical sections are **long enough** that running them concurrently is a real win (the lock's own overhead must be amortized). It is **reentrant**, supports **lock downgrading** (write→read) and **Conditions**. Caveats: higher per-acquire cost than synchronized, **writer starvation** possible in non-fair mode, and **no benefit** (often a loss) when writes are frequent. ### 3. `StampedLock` — hot, short, read-mostly paths Adds the **optimistic read**: read with no lock, then `validate`. This removes the compare-and-swap that even a shared read lock performs, eliminating cross-core cache contention on the lock state. Choose it when: - Sections are **very short** and **read-mostly**, and the read-lock overhead itself is the measured bottleneck, **and** - You do **not** need reentrancy, and do **not** need `Condition`s. Price: **non-reentrant** (self-deadlock risk), **no Conditions**, and the discipline of snapshot-into-locals-then-validate plus correct stamp handling. It also offers **mode conversion** for safe-ish upgrades. It is an expert tool; misuse is easy. ### 4. Sometimes: no lock at all For a **single** variable, prefer `volatile` (visibility only) or an `Atomic*` class / `VarHandle` (atomic compound updates via CAS) — these beat any lock for that narrow case. For an aggregate read-only after construction, make it **immutable** and skip locking entirely. For high-contention counters, `LongAdder` outperforms a locked `long`. ### A practical decision flow 1. Can the state be **immutable** or confined to one thread? → No lock needed. 2. Is it a **single field** with simple atomic updates? → `volatile` / `Atomic*` / `LongAdder`. 3. Otherwise, default to **`synchronized`**. 4. Profiling shows **read-heavy** contention with non-trivial read sections? → **`ReentrantReadWriteLock`**. 5. Profiling shows the **read-lock overhead itself** dominates a hot, tiny, read-mostly path, and you need neither reentrancy nor Conditions? → **`StampedLock`** with optimistic reads. ### Why 'measure first' is the recurring theme Read/write and stamped locks add bookkeeping. Under frequent writes every reader is blocked anyway, so the extra machinery is pure loss versus `synchronized`. Under tiny sections the lock overhead dwarfs the protected work. Benchmarks (ideally JMH, with representative read/write ratios and thread counts) routinely overturn intuition — the 'fancier' lock is frequently slower. Pick the simplest primitive that meets the need, then upgrade only with evidence.
- Give a concrete workload where ReentrantReadWriteLock helps and one where it hurts.Helps: a configuration or reference-data cache read by many request threads and refreshed rarely — readers run in parallel. Hurts: a counter or queue updated on nearly every operation — every write blocks all readers anyway and the read/write lock just adds overhead versus synchronized or an Atomic type.
- What would steer you away from StampedLock even on a read-heavy path?Needing reentrancy (recursive or call-out code under the lock), needing Condition-based wait/signal coordination, or read sections long enough that a plain read lock's overhead is negligible. Any of these makes ReentrantReadWriteLock or synchronized the safer, clearer choice.
saying these in an interview costs you the question
- Reaching for StampedLock or ReadWriteLock by default 'because it's faster'
- Using a ReadWriteLock under write-heavy or tiny-section workloads (it loses to synchronized)
- Picking StampedLock when the code needs reentrancy or Condition waiting
- Using a lock for a single counter where Atomic/LongAdder/volatile suffices