Contents

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 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.