Skip to content

Read-Write & Stamped Locks ​

This post explains Java's ReadWriteLock (via ReentrantReadWriteLock) and StampedLock (Java 8), when to use them, and how they compare to other synchronization primitives. It includes example implementations for a simple cache and interview-focused guidance.


Why read-write locks? ​

Many systems are read-heavy: multiple threads concurrently read the same data while writes are rare. A ReadWriteLock allows:

  • Multiple readers to access the data concurrently (no mutual exclusion for reads).
  • A single writer to have exclusive access (writers block readers and other writers).

This improves throughput for read-heavy workloads compared to a single mutual-exclusion lock (synchronized or ReentrantLock) which serializes both reads and writes.


ReentrantReadWriteLock ​

Key features ​

  • Two separate locks: readLock() and writeLock().
  • Reentrant: a thread can reacquire the same lock it already holds.
  • Supports fairness policy via constructor (fair vs non-fair).

Basic usage ​

java
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.Map;
import java.util.HashMap;

public class ReadWriteCache<K,V> {
    private final Map<K,V> map = new HashMap<>();
    private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();

    public V get(K key) {
        rw.readLock().lock();
        try {
            return map.get(key);
        } finally {
            rw.readLock().unlock();
        }
    }

    public void put(K key, V value) {
        rw.writeLock().lock();
        try {
            map.put(key, value);
        } finally {
            rw.writeLock().unlock();
        }
    }
}

Use cases ​

  • Thread-safe caches (many reads, few writes).
  • Config data readers updated rarely.
  • File metadata access where reads dominate.

Trade-offs & caveats ​

  • Write starvation: continuous readers can starve writers (unless fair lock is used).
  • Complexity: more error-prone than synchronized if not careful with lock ordering.
  • Overhead: acquiring read locks has some overhead; beneficial only when reads significantly outnumber writes.

StampedLock (Java 8) ​

StampedLock provides three modes:

  • Write lock (writeLock()): exclusive, like writeLock above.
  • Read lock (readLock()): shared read lock.
  • Optimistic read (tryOptimisticRead()): a non-blocking read that may be validated later.

Optimistic read pattern ​

Optimistic reads are fast because they don't block writers. After reading, the thread must validate that no write occurred during the read using validate(stamp).

Example:

java
import java.util.concurrent.locks.StampedLock;
import java.util.Map;
import java.util.HashMap;

public class StampedCache<K,V> {
    private final Map<K,V> map = new HashMap<>();
    private final StampedLock sl = new StampedLock();

    public V get(K key) {
        long stamp = sl.tryOptimisticRead();
        V value = map.get(key);
        if (!sl.validate(stamp)) {
            // Fallback to read lock
            stamp = sl.readLock();
            try {
                value = map.get(key);
            } finally {
                sl.unlockRead(stamp);
            }
        }
        return value;
    }

    public void put(K key, V value) {
        long stamp = sl.writeLock();
        try {
            map.put(key, value);
        } finally {
            sl.unlockWrite(stamp);
        }
    }
}

Benefits ​

  • Lower contention for reads: optimistic reads avoid locking in the common case.
  • Performance: very effective for high-read low-write workloads with short critical sections.

Caveats ​

  • Not reentrant: StampedLock is not reentrant; the same thread cannot reacquire a stamp.
  • Complexity: optimistic pattern requires validation and a fallback path, increasing code complexity.
  • Spinning & retries: in high-write scenarios, optimistic reads often fail and fall back, reducing benefit.

Example: Simple cache with expiry using ReentrantReadWriteLock ​

java
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.Map;
import java.util.HashMap;

public class ExpiringCache<K,V> {
    private final Map<K, V> map = new HashMap<>();
    private final Map<K, Long> expiry = new HashMap<>();
    private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();

    public V get(K key) {
        rw.readLock().lock();
        try {
            Long exp = expiry.get(key);
            if (exp == null || exp < System.currentTimeMillis()) {
                return null;
            }
            return map.get(key);
        } finally {
            rw.readLock().unlock();
        }
    }

    public void put(K key, V value, long ttlMillis) {
        rw.writeLock().lock();
        try {
            map.put(key, value);
            expiry.put(key, System.currentTimeMillis() + ttlMillis);
        } finally {
            rw.writeLock().unlock();
        }
    }
}

Interview Focus: When to prefer RWLock or StampedLock? ​

  • Prefer ReentrantReadWriteLock when:

    • Reads greatly outnumber writes.
    • Simpler semantics and reentrancy are beneficial.
    • You need fairness options to avoid writer starvation.
  • Consider StampedLock when:

    • You need the absolute best read throughput.
    • You can handle non-reentrancy and added complexity.
    • Read operations are short and you can tolerate occasional retry/fallback.
  • Use synchronized / simple locks when:

    • Simplicity is more important than throughput.
    • Contention is low or predictable.

Talking points in interviews ​

  • Explain why RW locks increase concurrency for read-heavy workloads.
  • Describe write-starvation and how fairness vs bias affects performance.
  • Show understanding of optimistic reads (StampedLock) and when they fail (frequent writes).
  • Discuss reentrancy and why StampedLock is not suitable for code that relies on reentrant behavior.

Performance considerations ​

  • Measure before optimizing: lock overhead vs contention matters.
  • For highly read-heavy workloads, RW locks usually help — but if reads are extremely cheap, the lock overhead may not pay off.
  • For mixed workloads, profile optimistic reads vs read-lock fallback under realistic write rates.

Summary ​

  • ReentrantReadWriteLock: easy-to-use read/write separation; reentrant; appropriate for many caches/config data.
  • StampedLock: offers optimistic reads for improved throughput but comes with complexity and no reentrancy.
  • Choose the tool based on read/write ratio, need for reentrancy, and code complexity tolerance.