ReadWriteLock & StampedLock
ReentrantReadWriteLock allows many concurrent readers OR one writer. StampedLock adds an optimistic-read mode that does not block writers. Use ReadWriteLock when reads dominate writes by 10x or more. Use StampedLock when reads vastly dominate and a retry is tolerable. Both are sharper than they look.
What it is
Java's ReentrantLock and synchronized give mutual exclusion: one thread in the section, everyone else waits. That is wasteful when most callers are just reading. Two specialised locks address this: ReentrantReadWriteLock and StampedLock.
ReentrantReadWriteLock
Two locks bundled together: a read lock and a write lock. Multiple threads can hold the read lock at once. Only one can hold the write lock, and while it does, no readers can hold theirs. The classic single-writer-many-readers contract.
Reentrant means a thread that holds the lock can re-acquire it. Both the read and write locks are individually reentrant. A thread holding the write lock can also acquire the read lock (downgrade). A thread holding the read lock cannot acquire the write lock; that deadlocks against itself.
When it pays off: read-heavy workloads where the read holds the lock for long enough that contention with writes is a real cost. Configuration caches, route tables, in-memory indexes that get rebuilt occasionally.
When it does not: read-dominated workloads where the read is so short that the bookkeeping (incrementing the read counter, the cache line traffic between cores) costs more than the saved exclusion. Or write-heavy workloads where a plain mutex would have been fine.
StampedLock
A different design. Three modes: optimistic read, pessimistic read, write. Each lock acquisition returns a long stamp; the caller passes it back when releasing.
The killer feature is optimistic read. Instead of acquiring a lock, the code records the current version, does the read, then validates that no writer ran during the read. If validation passes, the read is done at zero lock acquisition cost. If it fails, the code falls back to a real read lock and retries.
For read-heavy code with cheap reads (a few field loads), this can be roughly twice as fast as ReadWriteLock under contention.
The catch: the optimistic read can run while a writer is mutating, so the values read may be inconsistent. Validation indicates whether to discard them, but that only works when the read had no side effects. Pure computations only.
Also: StampedLock is not reentrant. Holding any stamp and acquiring another self-deadlocks.
When to reach for which
Plain synchronized or ReentrantLock for: short critical sections, write-heavy workloads, anything that needs reentrancy, simple shared state.
ReentrantReadWriteLock for: read-dominated workloads where reads are non-trivial in length, simple semantics are preferred, and reentrancy bookkeeping is fine.
StampedLock for: read-heavy workloads with very cheap, side-effect-free reads, where the optimistic path has been measured to win. Geometry, config caches, lookups.
ConcurrentHashMap or other lock-free structures for: many of the cases where a ReadWriteLock around a HashMap would otherwise appear. The standard library already did the hard work; reach for it before rolling a custom scheme.
A common mistake
People reach for ReadWriteLock by default, assuming "reads are free, so let's allow many of them". For short critical sections, the reader counter bookkeeping can cost more than the mutual exclusion. The lock-free read sounds cheaper than it actually is in cache terms.
The right approach is to start with synchronized, profile, and only swap to ReadWriteLock when the lock is a measured hot spot AND reads dominate AND reads are long enough to matter. The decision is rarely obvious from the code alone.
Primitives by language
- ReentrantReadWriteLock (read lock + write lock, fair/unfair)
- StampedLock (optimistic + read + write modes, returns long stamp)
- tryConvertToWriteLock / tryConvertToReadLock (StampedLock upgrade/downgrade)
Implementation
Many readers, occasional writer. With a plain mutex, every read serialises. With a ReadWriteLock, readers run in parallel; the writer blocks until all readers finish, then writes alone. Throughput on read-heavy workloads can improve dramatically.
1 import java.util.concurrent.locks.ReentrantReadWriteLock;
2
3 class ConfigCache {
4 private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
5 private final ReentrantReadWriteLock.ReadLock r = lock.readLock();
6 private final ReentrantReadWriteLock.WriteLock w = lock.writeLock();
7
8 private Map<String, String> config = Map.of();
9
10 public String get(String key) {
11 r.lock();
12 try { return config.get(key); }
13 finally { r.unlock(); }
14 }
15
16 public void reload(Map<String, String> next) {
17 w.lock();
18 try { config = Map.copyOf(next); }
19 finally { w.unlock(); }
20 }
21 }The optimistic read does not acquire any lock. It snapshots the version, reads the fields, then validates that no writer changed things during the read. If validation fails, fall back to a real read lock and retry. For read-mostly workloads with cheap reads, this can be twice as fast as ReadWriteLock.
1 import java.util.concurrent.locks.StampedLock;
2
3 class Point {
4 private double x, y;
5 private final StampedLock lock = new StampedLock();
6
7 public double distanceFromOrigin() {
8 long stamp = lock.tryOptimisticRead(); // no actual lock
9 double cx = x, cy = y; // read fields
10 if (!lock.validate(stamp)) { // any writer between?
11 stamp = lock.readLock(); // fall back to real read
12 try { cx = x; cy = y; }
13 finally { lock.unlockRead(stamp); }
14 }
15 return Math.sqrt(cx * cx + cy * cy);
16 }
17
18 public void move(double dx, double dy) {
19 long stamp = lock.writeLock();
20 try { x += dx; y += dy; }
21 finally { lock.unlockWrite(stamp); }
22 }
23 }Read first; on discovering a write is needed, try to convert the stamp without releasing the lock. If the conversion fails (another thread holds the write lock), release and retry from a write lock. Avoids the read-then-release-then-write race.
1 void moveIfAtOrigin(StampedLock lock, double newX, double newY) {
2 long stamp = lock.readLock();
3 try {
4 while (x == 0.0 && y == 0.0) {
5 long writeStamp = lock.tryConvertToWriteLock(stamp);
6 if (writeStamp != 0L) {
7 stamp = writeStamp;
8 x = newX; y = newY;
9 return;
10 }
11 lock.unlockRead(stamp);
12 stamp = lock.writeLock();
13 }
14 } finally {
15 lock.unlock(stamp);
16 }
17 }A thread holding the read lock that tries to acquire the write lock will deadlock. The write lock waits for all readers (including itself) to release. Same thread, same lock, hangs forever.
1 ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
2
3 void broken() {
4 lock.readLock().lock();
5 try {
6 if (needsWrite()) {
7 lock.writeLock().lock(); // DEADLOCK with itself
8 try { write(); } finally { lock.writeLock().unlock(); }
9 }
10 } finally {
11 lock.readLock().unlock();
12 }
13 }
14
15 // Correct: drop read lock, acquire write lock, re-check
16 void fixed() {
17 lock.readLock().lock();
18 boolean needs;
19 try { needs = needsWrite(); }
20 finally { lock.readLock().unlock(); }
21
22 if (needs) {
23 lock.writeLock().lock();
24 try { if (needsWrite()) write(); }
25 finally { lock.writeLock().unlock(); }
26 }
27 }Key points
- •ReentrantReadWriteLock: many readers OR one writer at a time. Readers do not block readers.
- •Read locks are reentrant. Write locks are reentrant. Holding write lock and acquiring read = OK. Holding read lock and acquiring write = DEADLOCK on the same thread.
- •StampedLock has optimistic read: no lock acquisition, validate after the read. Falls back to read lock if validation fails.
- •StampedLock is NOT reentrant. Calling readLock twice on the same thread deadlocks.
- •Both are heavier than synchronized for low-contention or write-dominant code. Measure.
Follow-up questions
▸When is ReadWriteLock actually faster than synchronized?
▸When to use StampedLock?
▸Why is StampedLock not reentrant?
▸Can readers starve writers?
Gotchas
- !Read lock cannot be upgraded to write lock on ReentrantReadWriteLock. Drop and re-acquire.
- !StampedLock readers must be side-effect-free and cheap. Optimistic read may execute and then re-execute.
- !StampedLock is NOT reentrant. Holding read or write and re-acquiring the same hangs.
- !Unfair ReadWriteLock can starve writers under sustained read load.
- !For very short critical sections, plain synchronized often beats both.