Contents
- The Three Lock Modes
- Write Lock and Read Lock
- Optimistic Read — The Key Feature
- Converting Between Modes
- StampedLock vs ReentrantReadWriteLock
- Write lock — exclusive access; no readers or other writers allowed. Blocks until all current readers/writers finish.
- Read lock (pessimistic) — shared access; multiple readers can hold it simultaneously; blocks writers.
- Optimistic read — does not acquire a lock; returns a stamp you can validate later. If a write occurred between the read and the validate call, you retry with a proper read lock.
import java.util.concurrent.locks.StampedLock;
StampedLock lock = new StampedLock();
// Every lock acquisition returns a stamp (long)
// stamp == 0 means the lock was not acquired (try* methods)
long writeStamp = lock.writeLock(); // acquires write lock
lock.unlockWrite(writeStamp); // releases with the stamp
long readStamp = lock.readLock(); // acquires read lock
lock.unlockRead(readStamp);
long optStamp = lock.tryOptimisticRead(); // no lock — just a stamp
boolean valid = lock.validate(optStamp); // true if no write happened
writeLock() acquires exclusive access and returns a long stamp; you must pass that exact stamp to unlockWrite() to release it. readLock() works the same way for shared reads — multiple threads can hold read locks simultaneously, but a write lock blocks all of them. tryOptimisticRead() is different: it returns a stamp immediately without acquiring any lock. You read the shared data, then call validate(stamp) — if it returns false, a write occurred between the read and the validate, and you must fall back to a full readLock().
class Point {
private double x, y;
private final StampedLock lock = new StampedLock();
// Write lock — exclusive
public void move(double dx, double dy) {
long stamp = lock.writeLock();
try {
x += dx;
y += dy;
} finally {
lock.unlockWrite(stamp);
}
}
// Read lock — shared, pessimistic (multiple readers OK, blocks writers)
public double distanceFromOriginPessimistic() {
long stamp = lock.readLock();
try {
return Math.sqrt(x * x + y * y);
} finally {
lock.unlockRead(stamp);
}
}
// Timed try — non-blocking attempt
public boolean tryMove(double dx, double dy, long timeout, TimeUnit unit)
throws InterruptedException {
long stamp = lock.tryWriteLock(timeout, unit);
if (stamp == 0) return false; // couldn't acquire
try {
x += dx; y += dy;
return true;
} finally {
lock.unlockWrite(stamp);
}
}
}
Optimistic read is the most important mode: it reads without locking, then validates. If validation fails (a write happened), it retries with a full read lock. This is a significant win for read-heavy scenarios.
class Point {
private double x, y;
private final StampedLock lock = new StampedLock();
// Optimistic read — zero overhead on the happy path
public double distanceFromOrigin() {
long stamp = lock.tryOptimisticRead(); // returns a stamp, no blocking
// Read the fields — may see inconsistent state if a write occurs
double curX = x, curY = y;
if (!lock.validate(stamp)) {
// A write occurred — fall back to a proper read lock
stamp = lock.readLock();
try {
curX = x;
curY = y;
} finally {
lock.unlockRead(stamp);
}
}
// Now curX and curY are consistent
return Math.sqrt(curX * curX + curY * curY);
}
}
Always copy fields into local variables before calling validate(). You must not use the field values directly after validation fails — re-read them under the read lock. Also, the code between tryOptimisticRead() and validate() must not have side effects, because it may execute multiple times.
StampedLock supports atomic lock conversion — upgrading from optimistic or read to write without releasing the lock:
class Point {
private double x, y;
private final StampedLock lock = new StampedLock();
// Try to upgrade from read lock to write lock atomically
public void moveIfAtOrigin(double newX, double newY) {
long stamp = lock.readLock();
try {
while (x == 0.0 && y == 0.0) {
// Try to convert read → write (atomic, returns 0 if fails)
long ws = lock.tryConvertToWriteLock(stamp);
if (ws != 0L) {
// Upgrade succeeded
stamp = ws;
x = newX;
y = newY;
break;
} else {
// Upgrade failed — release read lock, acquire write lock
lock.unlockRead(stamp);
stamp = lock.writeLock();
}
}
} finally {
lock.unlock(stamp); // unlock() works for any mode
}
}
// tryConvertToReadLock — downgrade from write to read
public void computeAndRead(double dx, double dy) {
long stamp = lock.writeLock();
try {
x += dx; y += dy;
// Downgrade to read lock — still holds the lock
stamp = lock.tryConvertToReadLock(stamp);
double d = Math.sqrt(x * x + y * y);
System.out.println("Distance after move: " + d);
} finally {
lock.unlock(stamp);
}
}
}
- StampedLock — not reentrant, no Condition support, optimistic reads, higher throughput in read-heavy workloads.
- ReentrantReadWriteLock — reentrant (same thread can acquire again), supports Condition variables, simpler API, but no optimistic reads.
// StampedLock is NOT reentrant — this deadlocks!
StampedLock sl = new StampedLock();
long s1 = sl.readLock();
long s2 = sl.readLock(); // DEADLOCK — same thread acquiring read lock twice
sl.unlockRead(s2);
sl.unlockRead(s1);
// StampedLock does not support Condition variables
// Use ReentrantLock or synchronized + wait/notify instead
// Performance guideline:
// - Read >> Write traffic (90%+ reads): StampedLock with optimistic reads
// - Roughly balanced or write-heavy: ReentrantReadWriteLock or ReentrantLock
// - Need reentrancy or Conditions: ReentrantReadWriteLock
// For new code with virtual threads, consider simpler locking
// — virtual threads make blocking cheap, reducing the need for optimistic tricks
StampedLock is best used in low-level, performance-critical code (e.g., high-throughput caches, geometry/spatial data). For most application code, ReentrantReadWriteLock or even plain synchronized is easier to reason about and less error-prone.