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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
