A ReadWriteLock splits one lock into a shared read lock (many threads can hold it simultaneously) and an exclusive write lock (only one holder, and no readers concurrently). Use it when reads massively outnumber writes and each critical section does enough work to justify the bookkeeping — otherwise it loses to plain synchronized.
Usage
private final ReadWriteLock rw = new ReentrantReadWriteLock();
private final Lock r = rw.readLock();
private final Lock w = rw.writeLock();
private Map<String, String> cache = new HashMap<>();
String get(String key) {
r.lock();
try { return cache.get(key); } // many readers run concurrently
finally { r.unlock(); }
}
void put(String key, String val) {
w.lock();
try { cache.put(key, val); } // exclusive: no readers, no writers
finally { w.unlock(); }
}
Pitfalls
- No read-to-write upgrade. Holding the read lock and calling
writeLock().lock()deadlocks — the writer waits for all readers (including you) to release. You must drop the read lock first, then take the write lock, and re-check state because another writer may have run in between. Downgrade (write → read) is allowed. - Writer starvation under read load. With the default (non-fair) lock, a steady stream of readers can keep a waiting writer blocked.
ReentrantReadWriteLockmitigates this: a writer in the queue blocks new readers from barging, but it is still a risk under heavy read traffic. - Overhead. Tracking shared reader counts (packed into the high 16 bits of AQS state) costs more than a simple mutex. For short critical sections the extra CAS traffic makes it slower than
synchronized.
Lock downgrading
The legitimate fine-grained pattern: acquire write, mutate, acquire read while still holding write, then release write — you now hold only read and other readers can join while you finish reading consistent state.