Concurrent Readers, Exclusive Writers
A read-write lock (rwlock) is a synchronization primitive that allows multiple concurrent readers OR one exclusive writer, but never both at the same time. This is a significant improvement over a plain mutex for workloads where reads vastly outnumber writes, because a mutex serializes all access -- even reads that do not modify data and could safely proceed in parallel.
The Core Invariant
At any moment, a read-write lock is in one of three states:
| State | Who can enter |
|---|---|
| Unlocked | Any thread (reader or writer) can acquire |
| Read-locked (N readers inside) | More readers can enter; writers must wait |
| Write-locked (1 writer inside) | Everyone else (readers and writers) must wait |
Reader Count Tracking
The rwlock internally maintains a reader count. When a thread acquires a read lock, the count increments. When it releases, the count decrements. A writer can only acquire the lock when the reader count is zero. This counting is typically done atomically (using CAS or fetch-and-add) to avoid requiring a secondary mutex.
Read-Preferring vs. Write-Preferring
Read-preferring rwlocks allow new readers to acquire the lock even while a writer is waiting. This maximizes read throughput but can starve writers -- if readers arrive continuously, the writer may wait indefinitely.
Write-preferring rwlocks block new readers once a writer is waiting. Current readers finish, and then the writer proceeds. New readers queue behind the writer. This prevents writer starvation but may reduce read throughput temporarily.
Most production implementations (including pthread_rwlock with PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP and Java's ReentrantReadWriteLock in fair mode) default to write-preferring to avoid starvation.
Upgradable Read Locks
Some rwlock implementations support lock upgrading -- a thread holding a read lock can atomically upgrade to a write lock without releasing and re-acquiring. This is tricky: if two readers both try to upgrade simultaneously, each waits for the other to release, causing deadlock. Therefore, most implementations either disallow upgrading or require that at most one "upgradable read lock" exists.
Use Cases
Read-write locks shine when data is read far more often than written:
- Configuration objects -- loaded at startup or updated infrequently, read on every request.
- In-memory caches -- lookups are extremely frequent, invalidations are rare.
- DNS lookup tables -- updated periodically, queried thousands of times per second.
- Routing tables -- modified when topology changes, read for every packet forwarded.
Java's ReadWriteLock and StampedLock
Java provides ReentrantReadWriteLock in java.util.concurrent.locks. It supports fair and non-fair modes, reentrant locking, and lock downgrading (write lock to read lock).
Java 8 introduced StampedLock, which adds optimistic reading. An optimistic read does not acquire any lock at all -- it reads the data and then checks a stamp to verify no writer intervened. If the stamp is still valid, the read was consistent without any locking overhead. If invalid, the thread falls back to a regular read lock. This eliminates reader-writer contention entirely for the common case where no write is in progress.
Real-Life: Configuration Cache
Consider a web application that loads configuration from a database into an in-memory Map<String, String>:
Without rwlock (using a plain mutex):
Thread 1: lock(); value = config.get("timeout"); unlock();
Thread 2: lock(); value = config.get("maxRetries"); unlock(); // BLOCKED by Thread 1
Thread 3: lock(); value = config.get("host"); unlock(); // BLOCKED by Thread 2
All three threads are serialized, even though they are all reading. On a server with 200 request-handling threads, this becomes a severe bottleneck.
With rwlock:
Thread 1: readLock(); value = config.get("timeout"); readUnlock();
Thread 2: readLock(); value = config.get("maxRetries"); readUnlock(); // runs in parallel!
Thread 3: readLock(); value = config.get("host"); readUnlock(); // runs in parallel!
Admin: writeLock(); config.put("timeout", "30s"); writeUnlock(); // exclusive
All reader threads proceed concurrently. Only when the admin updates the config does the write lock serialize access. Since config changes happen once per hour and reads happen 10,000 times per second, the rwlock eliminates 99.99% of the contention.
StampedLock optimistic read:
long stamp = lock.tryOptimisticRead();
String value = config.get("timeout");
if (!lock.validate(stamp)) {
stamp = lock.readLock(); // fallback
value = config.get("timeout");
lock.unlockRead(stamp);
}
The optimistic read is essentially free -- no atomic operations, no cache-line bouncing between cores. It only falls back to a real read lock if a writer was active during the read.