skip to content

How does StampedLock's optimistic read mode work, and how is it different from acquiring a read lock?

level: seniorimportance: should knowfreq 48%

answer

  1. tryOptimisticRead → read into locals → validate
  2. No lock acquired; writers never blocked
  3. Copy fields to locals BEFORE validate (torn reads)
  4. validate false → fall back to readLock()
  5. Stamp 0 = unavailable; not reentrant, no Conditions

basics

~20 s

An optimistic read takes no lock at all. You get a 'stamp' (a number), read the data, then call validate(stamp). If no writer changed the data in between, validate returns true and your read was fine; if it returns false, you fall back to a real read lock and read again.

solid answer

~50 s

StampedLock.tryOptimisticRead() returns a long 'stamp' without blocking and without taking any lock — it's a snapshot of the lock's version. You read the fields into local variables, then call validate(stamp). validate returns true only if no write lock was acquired since the stamp was issued, meaning your reads were consistent. If it returns false (a writer intervened, or possibly is in progress), you discard the values and fall back to a genuine readLock(), re-read, and unlock. Because the optimistic path acquires nothing, it has essentially zero contention overhead and is excellent for short, read-mostly critical sections. The cost: you must copy fields into locals before validating (never act on data while still unvalidated), validate can spuriously fail, and a stamp of 0 means optimistic reading is unavailable. It is not reentrant and offers no Condition support.

go deeper

for a junior

Understands that an optimistic read tries to read without locking and then checks whether a writer interfered.

for a middle

Can write the tryOptimisticRead → read → validate → fallback-to-readLock pattern and knows validate(false) means retry under a real lock.

for a senior

Explains the torn-read hazard requiring local copies, why optimistic readers never block writers, the CAS-overhead saving, and stamp-0 semantics.

for a principal

Weighs StampedLock vs ReentrantReadWriteLock by contention and section size, accounts for non-reentrancy and missing Conditions, and designs the validate/retry path so unvalidated state can never escape.

### Setting the stage A classic `ReadWriteLock` still makes every reader **acquire and release** a lock — that involves a compare-and-swap (an atomic CPU operation) on a shared counter, which causes cache-line contention between cores even though readers don't conflict logically. For very short, very hot read sections, that bookkeeping is the bottleneck. `StampedLock` (Java 8, `java.util.concurrent.locks.StampedLock`) adds a third mode designed to remove it: the **optimistic read**. ### What a 'stamp' is Every StampedLock operation returns a `long` called a **stamp**. Think of it as a *version token*: it encodes the lock's current state/version. You pass the stamp back later to release or to validate. A stamp value of `0` is special: it means "failure / not available." ### The optimistic read protocol Optimistic means *assume no writer will interfere, and check afterward*. The pattern is: ```java StampedLock sl = new StampedLock(); double distanceFromOrigin() { long stamp = sl.tryOptimisticRead(); // (1) no lock taken; get a version token double curX = x, curY = y; // (2) copy shared fields into LOCALS if (!sl.validate(stamp)) { // (3) did a writer grab the write lock since (1)? stamp = sl.readLock(); // (4) fallback: take a real shared read lock try { curX = x; curY = y; // (5) re-read under protection } finally { sl.unlockRead(stamp); // (6) release with the stamp } } return Math.sqrt(curX * curX + curY * curY); } ``` Step by step: 1. `tryOptimisticRead()` returns a stamp **without acquiring anything** and without blocking. (It returns 0 if a write lock is currently held.) 2. You read the shared fields **into local variables**. This copy is essential — see the hazard below. 3. `validate(stamp)` returns `true` **only if no write lock has been acquired since the stamp was issued**. True means your local copies form a consistent snapshot. 4–6. If `validate` returns `false`, a writer intervened (or might have), so you abandon the optimistic attempt and fall back to a genuine `readLock()`, re-read, and `unlockRead(stamp)`. ### Why you MUST copy into locals before validating While you are inside the optimistic block, **a writer can be modifying the fields concurrently** — you hold no lock. If you computed on the live fields directly (`Math.sqrt(x*x + y*y)` reading `x` and `y` separately mid-write) you could mix an old `x` with a new `y`, producing a value that never existed (a *torn read*), or even loop forever / throw if a field is, say, an array index that goes out of bounds. By snapshotting into locals first and only **then** validating, you ensure: either the snapshot was consistent (validate true) or you throw it away (validate false). Never expose or act on unvalidated optimistic data. ### How it differs from a read lock | Aspect | Optimistic read | Read (pessimistic) lock | |---|---|---| | Acquires a lock? | No | Yes (shared) | | Blocks writers? | No | Yes — writers wait for readers | | Contention cost | Essentially none | A CAS on the shared state | | Can fail / retry? | Yes (validate false) | No | | Best for | Very short, read-mostly sections | Longer reads, or write-contended | Because the optimistic read blocks nobody, **writers are never delayed by optimistic readers** — a big throughput win in read-mostly workloads. The price is the validate-and-retry dance and the discipline around local copies. ### Caveats - **Not reentrant**: re-acquiring within the same thread can deadlock; StampedLock tracks no per-thread hold count. - **No Conditions**: unlike ReentrantReadWriteLock, it has no `Condition` objects (no await/signal). - **Stamp 0** from `tryOptimisticRead()` means optimistic reading isn't currently available (a writer holds the lock) — treat it as a failed validate and fall back. - It supports **mode conversion** (`tryConvertToWriteLock`, etc.) for upgrade/downgrade attempts, which return a new stamp or 0. ### When to reach for it Use optimistic reads for hot, tiny read paths over a few fields where writes are rare (geometry points, cached config, small data structures). For longer reads, frequent writes, or when you need reentrancy or conditions, prefer `ReentrantReadWriteLock` or plain `synchronized`.

  • Why is the local-variable copy inside the optimistic block mandatory?
    Because no lock is held, a writer may mutate the fields concurrently. Reading multiple fields directly could mix old and new values (a torn read). Snapshotting into locals first, then validating, guarantees the snapshot was consistent (validate true) or is discarded (validate false), so you never act on inconsistent data.
  • What does a stamp value of 0 returned by tryOptimisticRead() mean?
    It signals that an optimistic read is not currently available — typically because a write lock is held. A subsequent validate(0) returns false, so you take the read-lock fallback path.

saying these in an interview costs you the question

  • Computing on shared fields directly instead of copying into locals before validate
  • Saying optimistic read takes a shared lock (it takes none)
  • Forgetting to handle validate() returning false (the fallback path)
  • Assuming StampedLock is reentrant or a drop-in ReadWriteLock replacement

context