Fundamentals 18 min read

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.

Java Captain
Java Captain
Java Captain
Understanding Java ReadWriteLock: Theory, Implementation, and Usage

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.

JavaconcurrencyLockAQSreadwritelockThreadSafety
Java Captain
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.