Definition
- Write lock – exclusive, like ReentrantReadWriteLock's writeLock(). Only one thread at a time.
- Read lock – shared, like ReadWriteLock's readLock(). Multiple concurrent readers, but blocks writers.
- Optimistic read – no lock acquired.
tryOptimisticRead()returns a stamp (a volatile read of the internal version number). Read shared data into local variables.validate(stamp)checks if any write lock was acquired since the stamp was issued. If valid, use the local copies with zero lock overhead. If invalid, fall back to a regular readLock. - Why faster – ReadWriteLock's
readLock()requires a CAS on a shared reader count. Under 100 concurrent readers, all 100 threads CAS the same counter, causing cache-line contention. StampedLock's optimistic read is a single volatile read with zero contention. - Limitations – NOT reentrant (locking twice from the same thread causes deadlock). No
newCondition(). Optimistic reads must be SHORT because the longer the read, the higher the chance a write invalidates it.
At National Bank of India, ReadWriteLock is like a sign-in sheet at the vault door: every teller reading the balance register must sign in and sign out. Even though readers do not interfere with each other, the sign-in sheet itself becomes a bottleneck. StampedLock's optimistic read is like glancing at the register without signing in, noting the page number (stamp), reading the balance, then checking if anyone flipped the page since you looked (validate). If nobody did, your balance is correct with zero overhead. If someone did, you sign in properly and read again.
Understanding the Concepts
What is StampedLock?
StampedLock, introduced in Java 8, extends the concept of read-write locking with an optimistic read mode that avoids acquiring any lock at all. It provides three access modes, each returning a stamp that must be used to unlock or validate.
How do the three modes work?
package com.bank.concurrency;
import java.math.BigDecimal;
import java.util.concurrent.locks.StampedLock;
public class StampedLockDemo {
private final StampedLock sl = new StampedLock();
private BigDecimal balance = new BigDecimal("100000.00");
private BigDecimal interestRate = new BigDecimal("7.5");
// MODE 1: Write lock (exclusive)
public void debit(BigDecimal amount) {
long stamp = sl.writeLock();
try {
balance = balance.subtract(amount);
} finally {
sl.unlockWrite(stamp);
}
}
// MODE 2: Read lock (shared, blocks writers)
public BigDecimal getBalanceLocked() {
long stamp = sl.readLock();
try {
return balance;
} finally {
sl.unlockRead(stamp);
}
}
// MODE 3: Optimistic read (NO LOCK!)
public BigDecimal getBalanceOptimistic() {
long stamp = sl.tryOptimisticRead(); // get stamp, NO lock
BigDecimal localBalance = balance; // copy to local variable
BigDecimal localRate = interestRate; // copy to local variable
if (sl.validate(stamp)) { // any write since stamp?
return localBalance; // NO write: data valid!
}
// Write happened: fall back to read lock
stamp = sl.readLock();
try {
return balance;
} finally {
sl.unlockRead(stamp);
}
}
}
How is StampedLock different from ReadWriteLock?
| Feature | StampedLock | ReentrantReadWriteLock |
|---|---|---|
| Optimistic read | Yes (zero lock overhead) | No |
| Read lock CAS | No (volatile read only) | Yes (CAS on reader count) |
| Reentrant | No (deadlock on re-lock!) | Yes |
| Condition support | No | Yes (on writeLock) |
| Fairness option | No | Yes |
| Lock downgrade | Yes (write to read) | Yes |
| Lock upgrade | Yes (read to write via tryConvertToWriteLock) | No |
| Best for | 99:1 read:write ratio | 95:5 read:write ratio |
Why is optimistic read faster?
ReadWriteLock's readLock() must atomically increment a shared reader count using CAS. Under 100 concurrent reader threads, all 100 threads CAS the same shared counter, causing cache-line ping-pong and contention. StampedLock's tryOptimisticRead() is a single volatile read of the internal stamp (a long value that changes only when a write lock is acquired or released). Volatile reads do not modify shared state, so there is zero contention even with 1000 concurrent readers.
What are the rules for optimistic read?
You must copy all shared data to local variables before calling validate(). You cannot read shared data after validate() because a write could happen between validate() returning true and the subsequent read.
// WRONG: read after validate
long stamp = sl.tryOptimisticRead();
if (sl.validate(stamp)) {
return balance; // WRONG: write may happen between validate and return!
}
// CORRECT: copy before validate
long stamp = sl.tryOptimisticRead();
BigDecimal local = balance; // copy first
if (sl.validate(stamp)) {
return local; // use the copy
}
What are the common mistakes?
The most dangerous mistake is re-locking from the same thread. StampedLock is NOT reentrant. If a thread holds a read lock and tries to acquire a write lock (or vice versa), it deadlocks against itself.
// WRONG: re-locking causes DEADLOCK
long stamp = sl.readLock();
try {
long writeStamp = sl.writeLock(); // DEADLOCK! Same thread, not reentrant
} finally {
sl.unlockRead(stamp);
}
// CORRECT: use tryConvertToWriteLock for upgrade
long stamp = sl.readLock();
try {
long writeStamp = sl.tryConvertToWriteLock(stamp);
if (writeStamp != 0L) {
stamp = writeStamp; // upgraded!
balance = balance.add(interest);
} else {
sl.unlockRead(stamp);
stamp = sl.writeLock(); // full write lock
balance = balance.add(interest);
}
} finally {
sl.unlock(stamp);
}
When should you use StampedLock?
Use StampedLock when reads vastly outnumber writes (99:1 or higher) and reads are short. The optimistic path handles the common case with zero overhead, and the rare write invalidation triggers a fallback to a full read lock.
What are common interview follow-up questions?
- Can StampedLock replace ReentrantLock? No, because StampedLock is not reentrant and does not support Conditions.
- Is the optimistic stamp thread-safe? The stamp is a long value local to the reading thread. It does not need to be shared.
Visual Overview
StampedLock: Optimistic Read Flow
═════════════════════════════════
Reader Thread Writer Thread
───────────── ─────────────
stamp = tryOptimisticRead()
local = balance (copy)
validate(stamp)?
│
├── YES (no write) ──→ return local (zero lock!)
│
└── NO (write occurred)
│
▼
stamp = readLock() writeLock()
return balance balance -= amount
unlockRead(stamp) unlockWrite(stamp)
Key Takeaway
StampedLock adds optimistic reading to the read-write locking pattern. tryOptimisticRead() returns a stamp without acquiring any lock, just a volatile read of the internal version number. You copy shared data to local variables, then call validate(stamp). If no write lock was acquired since the stamp was issued, the data is valid with zero lock overhead. If a write occurred, you fall back to a regular readLock.
This beats ReentrantReadWriteLock for read-heavy workloads because ReadWriteLock's readLock() requires a CAS on a shared reader count that causes contention under many concurrent readers. StampedLock's optimistic read has zero contention because it only performs a volatile read. The trade-off is that StampedLock is not reentrant and does not support Conditions.
At National Bank of India, a balance cache serving 10,000 reads per second with only 10 writes per second achieved P99 latency of 0.1 milliseconds with StampedLock, compared to 2 milliseconds with ReadWriteLock. Over 99.9% of reads completed on the optimistic path with no lock overhead.
Remember one sentence: StampedLock's optimistic read gives zero-lock overhead for readers by validating a stamp instead of acquiring a lock, making it ideal for read-heavy workloads.
---