Java StampedLock: Optimistic Reads and High-Performance Locking

StampedLock, added in Java 8, improves on ReentrantReadWriteLock by introducing a third mode: optimistic reading. Optimistic reads don't block writers at all, instead, they check after reading whether a write occurred. In read-heavy workloads this can dramatically reduce contention. The trade-off is a more complex API built around stamps (long tokens) that track lock ownership.

Contents

The Three Lock Modes

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

Write Lock and Read Lock

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

Java
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: The Key Feature

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.

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

Converting Between Modes

StampedLock supports atomic lock conversion, upgrading from optimistic or read to write without releasing the lock:

Java
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 vs ReentrantReadWriteLock

  • 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.
Java
// 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.