ReadWriteLock
ReadWriteLock is an interface in java.util.concurrent.locks that maintains a pair of associated locks — one for read-only operations and one for write operations. The read lock may be held simultaneously by multiple reader threads as long as no writer holds the write lock. The write lock is exclusive: while a writer holds it, no other readers or writers may proceed. This segregation of read and write access enables much higher throughput than a mutual exclusion lock when reads are frequent and writes are infrequent — the common pattern for caches, configuration objects, routing tables, and reference data. The standard implementation is ReentrantReadWriteLock, which supports reentrancy for both locks, optional fairness, lock downgrading from write to read, and rich introspection. This entry covers the ReadWriteLock contract and when it outperforms a plain lock, ReentrantReadWriteLock's reentrancy and fairness behavior, lock downgrading, the write-lock starvation problem, lock upgrading (and why it is not supported), performance characteristics, and patterns for cache and configuration management.
ReadWriteLock Contract and ReentrantReadWriteLock
// ── Basic ReadWriteLock usage ─────────────────────────────────────────
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();
// ── Read operation: multiple readers can proceed simultaneously ────────
readLock.lock();
try {
// Any number of threads can be here simultaneously:
System.out.println("Reading: " + sharedData);
} finally {
readLock.unlock(); // always unlock in finally
}
// ── Write operation: exclusive — no other readers or writers ──────────
writeLock.lock();
try {
// Only one thread here at a time; all readers blocked:
sharedData = "new value";
} finally {
writeLock.unlock();
}
// ── Concurrent read demonstration ─────────────────────────────────────
String[] data = {"initial"};
ReentrantReadWriteLock dataLock = new ReentrantReadWriteLock();
Runnable reader = () -> {
dataLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " reading: " + data[0]);
Thread.sleep(200); // simulate slow read — other readers proceed simultaneously
System.out.println(Thread.currentThread().getName() + " done reading");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
dataLock.readLock().unlock();
}
};
// All three readers run concurrently (total time ~200ms, not ~600ms):
Thread r1 = new Thread(reader, "Reader-1");
Thread r2 = new Thread(reader, "Reader-2");
Thread r3 = new Thread(reader, "Reader-3");
r1.start(); r2.start(); r3.start();
r1.join(); r2.join(); r3.join();
// Reader-1 reading: initial
// Reader-2 reading: initial ← all three start nearly simultaneously
// Reader-3 reading: initial
// Reader-1 done reading
// Reader-2 done reading
// Reader-3 done reading
// ── Read lock does NOT support Condition ─────────────────────────────
try {
dataLock.readLock().newCondition(); // UnsupportedOperationException
} catch (UnsupportedOperationException e) {
System.out.println("Read lock has no Condition support");
}
// ── ReentrantReadWriteLock introspection ─────────────────────────────
ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
rw.readLock().lock();
System.out.println("Read lock count: " + rw.getReadLockCount()); // 1
System.out.println("Read hold count: " + rw.getReadHoldCount()); // 1 (this thread)
System.out.println("Write locked: " + rw.isWriteLocked()); // false
System.out.println("Write hold count: " + rw.getWriteHoldCount()); // 0
rw.readLock().unlock();Lock Downgrading, Starvation, and Performance
// ── Lock downgrading: write → read without window ─────────────────────
public class CachedData {
private volatile boolean dataValid = false;
private Object cachedData;
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock read = rwl.readLock();
private final Lock write = rwl.writeLock();
public Object getData() {
read.lock(); // acquire read lock first
if (!dataValid) {
read.unlock(); // release read — must do before acquiring write
write.lock(); // acquire write — exclusive
try {
// Re-check condition: another thread may have updated while we waited
if (!dataValid) {
cachedData = fetchData(); // only this thread here — update cache
dataValid = true;
}
read.lock(); // DOWNGRADE: acquire read while still holding write
} finally {
write.unlock(); // release write — readers can now proceed
}
// Now hold only read lock — downgrade complete, no window for other writers
}
try {
return cachedData; // safely read the just-updated (or pre-existing) value
} finally {
read.unlock();
}
}
private Object fetchData() { return new Object(); } // simulated fetch
}
// ── Lock UPGRADING causes deadlock — DO NOT DO ────────────────────────
ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
rw.readLock().lock();
try {
// rw.writeLock().lock(); // DEADLOCK: waiting for write, but holding read
// // write waits for all readers (including us) to release
// // we never release read because we're stuck waiting for write
} finally {
rw.readLock().unlock();
}
// CORRECT pattern for conditional upgrade: release read, acquire write, re-check:
rw.readLock().lock();
boolean needsUpdate;
try {
needsUpdate = !dataValid;
} finally {
rw.readLock().unlock(); // release read before acquiring write
}
if (needsUpdate) {
rw.writeLock().lock();
try {
if (!dataValid) { // re-check: another thread may have updated meanwhile
cachedData = fetchData();
dataValid = true;
}
} finally {
rw.writeLock().unlock();
}
}
// ── Writer starvation in unfair mode ──────────────────────────────────
// Unfair RWL: barging readers can continuously deny the waiting writer:
ReentrantReadWriteLock unfair = new ReentrantReadWriteLock(false);
// Scenario: readers R1-R100 keep arriving continuously.
// Writer W1 is waiting. In unfair mode:
// R2 sees "read lock held by R1" and joins (barges ahead of W1).
// R3 arrives, joins. ... R100 arrives, joins.
// W1 is never granted the write lock — starved.
// Fix: fair mode ensures queued writer is served before new readers:
ReentrantReadWriteLock fair = new ReentrantReadWriteLock(true);
// With fair=true: new readers queue behind W1 once W1 is waiting.
// Trade-off: lower read concurrency, but no writer starvation.
// ── Performance: when ReadWriteLock helps ─────────────────────────────
// ReadWriteLock is beneficial ONLY when:
// 1. Reads are much more frequent than writes
// 2. Read operations take non-trivial time (hold the lock long enough to matter)
// 3. Multiple readers can actually run concurrently (multi-core)
// Rule of thumb: benefit threshold ~80-90% reads, 10-20% writes on multi-core.
// Below that threshold, the overhead of separate read/write tracking may HURT performance.
// Benchmark scenario (read:write = 95:5, 8 threads):
// synchronized: ~50ms for 1M operations
// ReentrantLock: ~45ms
// ReentrantReadWriteLock: ~15ms ← significant win when reads dominatePractical Patterns — Cache, Configuration, and Routing Tables
// ── Read-heavy cache pattern ─────────────────────────────────────────
public class ReadHeavyCache<K, V> {
private final Map<K, V> cache = new HashMap<>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock readLock = rwl.readLock();
private final Lock writeLock= rwl.writeLock();
public V get(K key) {
readLock.lock();
try {
return cache.get(key); // multiple readers proceed concurrently
} finally {
readLock.unlock();
}
}
public void put(K key, V value) {
writeLock.lock();
try {
cache.put(key, value); // exclusive — all readers blocked during write
} finally {
writeLock.unlock();
}
}
public V computeIfAbsent(K key, Function<K, V> loader) {
// Optimistic: check with read lock first
readLock.lock();
try {
V existing = cache.get(key);
if (existing != null) return existing;
} finally {
readLock.unlock();
}
// Not found: upgrade to write lock to compute and insert
writeLock.lock();
try {
// Re-check: another thread may have inserted while we waited for write lock
return cache.computeIfAbsent(key, loader);
} finally {
writeLock.unlock();
}
}
public int size() {
readLock.lock();
try { return cache.size(); }
finally { readLock.unlock(); }
}
}
// ── Reloadable configuration ──────────────────────────────────────────
public class Configuration {
private volatile Map<String, String> config = Collections.emptyMap();
private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
// Called by request-handling threads — many concurrent readers:
public String get(String key) {
rw.readLock().lock();
try {
return config.getOrDefault(key, "");
} finally {
rw.readLock().unlock();
}
}
// Called periodically by a reload thread — exclusive:
public void reload(Map<String, String> newConfig) {
rw.writeLock().lock();
try {
config = Collections.unmodifiableMap(new HashMap<>(newConfig));
System.out.println("Configuration reloaded: " + config.size() + " keys");
} finally {
rw.writeLock().unlock();
}
}
}
// ── StampedLock: optimistic reads for maximum throughput ──────────────
import java.util.concurrent.locks.StampedLock;
public class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
public void move(double deltaX, double deltaY) {
long stamp = sl.writeLock(); // exclusive write lock
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
public double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead(); // no lock acquired — stamp only
double curX = x, curY = y; // read fields optimistically
if (!sl.validate(stamp)) { // check if a write occurred
// Optimistic read failed — fall back to read lock:
stamp = sl.readLock();
try {
curX = x;
curY = y;
} finally {
sl.unlockRead(stamp);
}
}
// Optimistic read succeeded — no lock was ever held:
return Math.sqrt(curX * curX + curY * curY);
}
}
// Performance profile: StampedLock optimistic reads have near-zero overhead
// when writes are rare — no CAS, no volatile read on the happy path.
// Suitable for: geospatial data, financial pricing, ML model inference.