Understanding Java ReadWriteLock: Theory, Implementation, and Usage
This article explains why read‑write locks are needed, presents a simple Java implementation, and dives into the inner workings of ReentrantReadWriteLock, covering state encoding, acquisition and release algorithms, lock downgrading, and fairness policies.
In a recent small project I needed a config.json file that is read and written concurrently, which led me to consider read‑write locks; this article reviews the concepts of read‑write locks.
Why do we need a read‑write lock?
Unlike a traditional exclusive lock, a read‑write lock allows shared reads but exclusive writes: “read‑read not mutually exclusive, read‑write mutually exclusive, write‑write mutually exclusive”. In many scenarios reads far outnumber writes, so a read‑write lock can improve performance.
Note that “reads far outnumber writes”. When contention is low, the extra bookkeeping of a read‑write lock may outweigh its benefits, so the choice depends on the actual workload.
A simple read‑write lock implementation
Based on the theory above, we can implement a rudimentary read‑write lock using two int variables. The implementation is intentionally simple but illustrates the core principles.
public class ReadWriteLock {
/** number of read locks held */
private int readCount = 0;
/** number of write locks held */
private int writeCount = 0;
/** acquire read lock – can only succeed when no write lock is held */
public synchronized void lockRead() throws InterruptedException {
while (writeCount > 0) {
wait();
}
readCount++;
}
/** release read lock */
public synchronized void unlockRead() {
readCount--;
notifyAll();
}
/** acquire write lock – must wait while any read lock exists */
public synchronized void lockWrite() throws InterruptedException {
while (writeCount > 0) {
wait();
}
writeCount++;
while (readCount > 0) {
wait();
}
}
/** release write lock */
public synchronized void unlockWrite() {
writeCount--;
notifyAll();
}
}Implementation principles of ReentrantReadWriteLock
In Java the standard implementation is ReentrantReadWriteLock , which offers features such as fairness selection, re‑entrancy, and lock downgrading.
Fairness: supports fair and non‑fair acquisition; non‑fair mode favors throughput.
Re‑entrancy: a thread that holds a read (or write) lock can reacquire the same lock.
Downgrading: a thread can acquire a read lock while holding the write lock, then release the write lock, remaining in read mode.
Structure of ReentrantReadWriteLock
The core of ReentrantReadWriteLock is a synchronizer Sync built on AbstractQueuedSynchronizer (AQS). Sync creates a ReadLock (shared) and a WriteLock (exclusive).
The constructor shows that both ReadLock and WriteLock share the same Sync instance, allowing a single queue to represent both shared and exclusive modes.
Sync implementation
The Sync state is a 32‑bit integer split into high 16 bits for read locks and low 16 bits for write locks. The following code extracts the shared and exclusive counts.
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }When a thread attempts to acquire a read lock, tryAcquireShared checks for an existing write lock, possible waiting writers, and then performs a CAS on the high 16 bits.
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 1. If a write lock exists and is owned by another thread, fail
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
// 2. If readers should block (fair mode or waiting writer), fail
if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
// 3. Record thread‑local read hold count
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}If the fast path fails, fullTryAcquireShared performs a loop that handles re‑entrancy and waiting writers.
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
// 5. Fail if a write lock exists owned by another thread
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
} else if (readerShouldBlock()) {
// handle firstReader fast‑path, otherwise queue
if (firstReader == current) { /* ok */ }
else {
if (rh == null) rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
if (rh.count == 0) return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {
// update thread‑local counters as in tryAcquireShared
// ... (omitted for brevity) ...
return 1;
}
}
}Read lock release
The release path clears the thread‑local counters and decrements the shared count via CAS.
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}Write lock acquisition
Write acquisition uses acquire → tryAcquire . The fast path checks for existing readers or writers and handles re‑entrancy.
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
setState(c + acquires);
return true;
}
if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}Write lock release
Releasing the write lock clears the exclusive owner when the count reaches zero and wakes up the next queued thread.
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}Lock downgrading
Downgrading occurs when a thread holding the write lock acquires the read lock before releasing the write lock, ensuring visibility of the protected state.
class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
if (!cacheValid) {
data = ...;
cacheValid = true;
}
// downgrade
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // still hold read lock
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}Fair vs. non‑fair mode
In fair mode both read and write threads must queue and acquire the lock in order; in non‑fair mode writers can barge, but readers may still be blocked if a writer is at the head of the queue to avoid writer starvation.
static final class FairSync extends Sync {
final boolean writerShouldBlock() { return hasQueuedPredecessors(); }
final boolean readerShouldBlock() { return hasQueuedPredecessors(); }
}
static final class NonfairSync extends Sync {
final boolean writerShouldBlock() { return false; } // writers can barge
final boolean readerShouldBlock() { return apparentlyFirstQueuedIsExclusive(); }
}Overall, the article walks through the motivation, a simple custom implementation, and the inner workings of Java’s ReentrantReadWriteLock , covering state encoding, acquisition/release algorithms, lock downgrading, and fairness policies.
Java Captain
Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.