ReentrantReadWriteLock mechanism

ReentrantReadWriteLock

A reentrant read-write lock that allows multiple concurrent readers OR a single exclusive writer. Built on a single AQS int state split into two 16-bit halves --- upper bits for shared (read) count, lower bits for exclusive (write) count.

Document Structure:

  • [Basic Usage](#Basic Usage) --- Quick start
  • [Packed State](#Packed State) --- How one int encodes both read and write counts
  • [Read Lock Deep Dive](#Read Lock Deep Dive) --- tryAcquireShared, per-thread hold counts, fullTryAcquireShared slow path
  • [Write Lock Deep Dive](#Write Lock Deep Dive) --- tryAcquire, reentrancy, why read-to-write upgrade deadlocks
  • [Lock Release](#Lock Release) --- tryReleaseShared (read), tryRelease (write), wake-up behavior
  • [Lock Downgrade](#Lock Downgrade) --- Safe write-to-read transition with timeline
  • [Fair vs Non-Fair](#Fair vs Non-Fair) --- readerShouldBlock(), writerShouldBlock(), writer starvation
  • [Memory Ordering and Happens-Before](#Memory Ordering and Happens-Before) --- Visibility guarantees, volatile state, CAS memory effects
  • [Condition Support](#Condition Support) --- Write lock only, why read lock can't have conditions
  • [tryLock and Interruptible Acquisition](#tryLock and Interruptible Acquisition) --- Non-blocking and timed acquisition, barging semantics
  • [Diagnostic and Monitoring APIs](#Diagnostic and Monitoring APIs) --- Hold counts, queue inspection, debugging tools
  • [Common Pitfalls and Anti-Patterns](#Common Pitfalls and Anti-Patterns) --- Mistakes beyond lock upgrade
  • [Common Patterns](#Common Patterns) --- Cache, config reload, lock downgrade for publish
  • [Performance Characteristics](#Performance Characteristics) --- When RRWL wins, when it loses, benchmarking guidance
  • FAQ --- Common questions answered

Basic Usage

java 复制代码
ReadWriteLock rwLock = new ReentrantReadWriteLock();

// Multiple readers can hold simultaneously
rwLock.readLock().lock();
try { /* read data */ }
finally { rwLock.readLock().unlock(); }

// Writer is exclusive --- blocks all readers and other writers
rwLock.writeLock().lock();
try { /* modify data */ }
finally { rwLock.writeLock().unlock(); }

Core rules:

  • Multiple threads can hold the read lock simultaneously (shared mode)
  • Only one thread can hold the write lock (exclusive mode)
  • Read lock and write lock are mutually exclusive (except for lock downgrade by the same thread)
  • Both locks are reentrant --- a thread can acquire the same lock multiple times

Class Hierarchy and Wiring

复制代码
ReentrantReadWriteLock
│
├── sync: Sync                          ← created once in constructor
│
├── ReadLock  (implements Lock)
│     └── sync ─── points to parent's sync (set in ReadLock constructor)
│
├── WriteLock (implements Lock)
│     └── sync ─── points to parent's sync (set in WriteLock constructor)
│
│   All three sync references → SAME object in memory
│
├── abstract Sync extends AbstractQueuedSynchronizer (AQS)
│     ├── tryAcquire / tryRelease           (write lock --- exclusive mode)
│     ├── tryAcquireShared / tryReleaseShared (read lock --- shared mode)
│     ├── sharedCount(), exclusiveCount()    (packed state helpers)
│     └── firstReader, cachedHoldCounter, readHolds (per-thread hold counts)
│
├── NonfairSync extends Sync                ← default
│     ├── writerShouldBlock()  → false (always barge)
│     └── readerShouldBlock()  → apparentlyFirstQueuedIsExclusive()
│
└── FairSync extends Sync
      ├── writerShouldBlock()  → hasQueuedPredecessors()
      └── readerShouldBlock()  → hasQueuedPredecessors()

Construction and wiring:

java 复制代码
public ReentrantReadWriteLock() {
    this(false);  // non-fair by default
}

public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();  // 1. create Sync
    readerLock = new ReadLock(this);                     // 2. wire ReadLock
    writerLock = new WriteLock(this);                    // 3. wire WriteLock
}

// ReadLock grabs sync from the parent --- same pattern for WriteLock
public static class ReadLock implements Lock {
    private final Sync sync;

    protected ReadLock(ReentrantReadWriteLock lock) {
        sync = lock.sync;  // points to the SAME Sync object
    }
}

ReadLock and WriteLock don't extend AQS --- they delegate to sync. The sync field is final in all three classes, wired once at construction. The fair/non-fair choice at construction time is the only way to control which Sync subclass is used.

Call chain: readLock.lock()sync.acquireShared(1)AQS.acquireShared() (inherited template) → Sync.tryAcquireShared() (subclass override). The only difference between fair and non-fair is the two shouldBlock methods --- everything else lives in the shared Sync base class.


Packed State --- Single int, Two Counters

Uses AQS with a single int state split into two 16-bit halves:

复制代码
state (32 bits):
┌────────────────┬────────────────┐
│ upper 16 bits  │ lower 16 bits  │
│ = read count   │ = write count  │
└────────────────┴────────────────┘

Bit manipulation:
  SHARED_SHIFT   = 16
  SHARED_UNIT    = 1 << 16  = 65536
  MAX_COUNT      = (1 << 16) - 1 = 65535
  EXCLUSIVE_MASK = (1 << 16) - 1

  Read count:  state >>> SHARED_SHIFT        (unsigned right shift)
  Write count: state & EXCLUSIVE_MASK        (mask lower 16 bits)

Why Pack Two Counters Into One int?

AQS provides a single int state field with CAS operations. CyclicBarrier needed a lock because it has multiple fields that must change atomically (count + generation + barrier action). ReentrantReadWriteLock avoids this by encoding both counters in one int --- a single CAS can atomically update either counter without a lock.

For example, acquiring the read lock is: CAS(state, c, c + SHARED_UNIT) --- this atomically increments the upper 16 bits while leaving the lower 16 bits unchanged. The write lock is: CAS(state, c, c + 1) --- increments the lower 16 bits only.

This is possible because the two counters are independent enough: you never need to change both simultaneously in a single CAS. Read lock acquisition only touches the upper bits; write lock acquisition only touches the lower bits.

State Transitions

复制代码
No locks:                    state = 0x0000_0000  (read=0, write=0)
1 writer:                    state = 0x0000_0001  (read=0, write=1)
Writer reentrant (3x):       state = 0x0000_0003  (read=0, write=3)
1 reader:                    state = 0x0001_0000  (read=1, write=0)
3 readers:                   state = 0x0003_0000  (read=3, write=0)
Writer + read (downgrade):   state = 0x0001_0001  (read=1, write=1)
Reentrant writer + read:     state = 0x0001_0002  (read=1, write=2)

Note on downgrade states: Both counters can be > 1, but only via reentrancy by the same thread . While the write lock is held, no other thread can acquire either lock --- tryAcquireShared fails on the exclusiveCount(c) != 0 && owner != current check, and tryAcquire fails on the ownership check. So read=1, write=2 means one thread called writeLock.lock() twice then readLock.lock() once --- not three different threads.

Reentrant downgrade timeline:

复制代码
Thread-A: writeLock.lock()   → state = 0x0000_0001  (read=0, write=1)
Thread-A: writeLock.lock()   → state = 0x0000_0002  (read=0, write=2)  ← reentrant
Thread-A: readLock.lock()    → state = 0x0001_0002  (read=1, write=2)  ← downgrade
Thread-A: writeLock.unlock() → state = 0x0001_0001  (read=1, write=1)
Thread-A: writeLock.unlock() → state = 0x0001_0000  (read=1, write=0)  ← write fully released
                               ↑ now other readers can enter
Thread-A: readLock.unlock()  → state = 0x0000_0000

Full State Machine --- All Lock and Unlock Transitions

Every possible transition between states, showing which operation triggers it and what conditions must hold:

复制代码
                            ┌─────────────────────────────────────────────────────────┐
                            │              STATE MACHINE LEGEND                        │
                            │  R = read count (upper 16 bits)                         │
                            │  W = write count (lower 16 bits)                        │
                            │  RL = readLock, WL = writeLock                          │
                            │  Thread-X = any thread, Thread-O = write lock owner     │
                            └─────────────────────────────────────────────────────────┘


═══════════════════════════════════════════════════════════════════════════════════════
 FROM STATE          OPERATION              TO STATE             WHO / CONDITION
═══════════════════════════════════════════════════════════════════════════════════════

 ── ACQUIRING FROM UNLOCKED ──────────────────────────────────────────────────────────

 R=0, W=0            WL.lock()              R=0, W=1             Any thread (CAS)
 R=0, W=0            RL.lock()              R=1, W=0             Any thread (CAS)

 ── WRITE LOCK REENTRANCY (same owner only) ──────────────────────────────────────────

 R=0, W=n            WL.lock()              R=0, W=n+1           Owner only (no CAS)
 R=0, W=n            WL.unlock()            R=0, W=n-1           Owner only (no CAS)
 R=0, W=1            WL.unlock()            R=0, W=0             Owner → clears owner
                                                                  → wakes queue head

 ── READ LOCK CONCURRENCY + REENTRANCY ───────────────────────────────────────────────

 R=n, W=0            RL.lock()              R=n+1, W=0           Any thread (CAS)
                                                                  [if !readerShouldBlock]
 R=n, W=0            RL.lock()              R=n+1, W=0           Reentrant reader
                                                                  [even if readerShouldBlock]
 R=n, W=0            RL.unlock()            R=n-1, W=0           Any reader (CAS loop)
 R=1, W=0            RL.unlock()            R=0, W=0             Last reader
                                                                  → wakes queue head

 ── LOCK DOWNGRADE (write owner acquires read) ──────────────────────────────────────

 R=0, W=n            RL.lock()              R=1, W=n             Owner only (CAS)
                                                                  [owner == current → allowed]
 R=m, W=n            RL.lock()              R=m+1, W=n           Owner only (CAS)
                                                                  [reentrant read during downgrade]
 R=m, W=n            WL.unlock()            R=m, W=n-1           Owner only (no CAS)
 R=m, W=1            WL.unlock()            R=m, W=0             Owner → clears owner
                                                                  → wakes queue head
                                                                  (spurious if R>0)
 R=1, W=0            RL.unlock()            R=0, W=0             Former owner
                                                                  → wakes queue head (real)

 ── BLOCKED TRANSITIONS (these FAIL, thread parks) ──────────────────────────────────

 R=n, W=0            WL.lock()              BLOCKED              Any thread
                                                                  [c!=0, w==0 → fail]
                                                                  [even if current holds reads]
 R=0, W=n            WL.lock()              BLOCKED              Non-owner thread
                                                                  [owner != current → fail]
 R=0, W=n            RL.lock()              BLOCKED              Non-owner thread
                                                                  [exclusiveCount!=0,
                                                                   owner!=current → fail]
 R=n, W=0            RL.lock()              BLOCKED              First-time reader
                                                                  [readerShouldBlock &&
                                                                   holdCount==0 → must queue]

═══════════════════════════════════════════════════════════════════════════════════════

Visual state diagram:

复制代码
                              WL.lock (any thread, CAS)
                    ┌──────────────────────────────────────┐
                    │                                      ▼
              ┌───────────┐                         ┌───────────┐
              │  R=0 W=0  │                         │  R=0 W=1  │◄──┐ WL.lock
              │ (unlocked)│                         │ (writer)  │───┘ (reentrant,
              └───────────┘                         └───────────┘     W→W+1)
                    │  ▲                                │  │
     RL.lock        │  │ last RL.unlock                 │  │
     (any, CAS)     │  │ (R=1→0, wakes queue)          │  │
                    ▼  │                                │  │
              ┌───────────┐                             │  │
         ┌───►│  R=n W=0  │    WL.lock: BLOCKED         │  │
         │    │ (readers) │    (c!=0, w==0 → fail)      │  │
         │    └───────────┘                             │  │
         │     RL.lock (concurrent/reentrant, CAS)      │  │
         └──────────────────────────────────────────────┘  │
                                                           │
                              RL.lock (owner only, CAS)    │
                    ┌──────────────────────────────────────┘
                    ▼
              ┌───────────┐
         ┌───►│  R=m W=n  │    Only the write lock owner thread exists here.
         │    │(downgrade)│    No other thread can acquire any lock.
         │    └───────────┘
         │         │  │
         │         │  │ WL.unlock (owner, no CAS, W→W-1)
         │         │  │ when W reaches 0 → clears owner → wakes queue
         │         │  │ (spurious wake if R>0 --- woken writer can't acquire)
         │         │  │
         │         │  └──► transitions to R=m W=0 (readers state above)
         │         │
         │         └─── RL.lock (owner reentrant, CAS, R→R+1)
         └──────────────────────────────────────────────────

Key rules summarized:

Operation Succeeds when Fails when
WL.lock() state==0 (CAS) or owner reentrant (w>0, owner==current) Any lock held by others, OR read locks held (even by self)
WL.unlock() Owner only. Wakes queue when w reaches 0 Non-owner → IllegalMonitorStateException
RL.lock() No write lock, or write lock held by current thread (downgrade) Write lock held by another thread. First-time reader when readerShouldBlock
RL.unlock() Any reader with holdCount>0. Wakes queue when total R reaches 0 holdCount==0IllegalMonitorStateException

Maximum Counts

Each counter is 16 bits, so the maximum is 65535 for both read holds and write holds. Exceeding this throws Error("Maximum lock count exceeded"). In practice, 65535 concurrent readers or 65535 reentrant write acquisitions is more than enough.

Design trade-off: 16 bits per counter is a deliberate choice. A 24/8 split would allow ~16M readers but only 255 reentrant writes. A 20/12 split would give ~1M readers and 4095 writes. The symmetric 16/16 split keeps both limits generous without favoring either mode. If you genuinely need more than 65535 concurrent readers, your bottleneck is elsewhere (context switching, cache coherence traffic) --- not the lock.


Read Lock Deep Dive

The read lock uses AQS shared mode. Multiple threads can hold it simultaneously.

ReadLock.lock() --- Entry Point

java 复制代码
// ReadLock.lock() → sync.acquireShared(1)
// AQS.acquireShared --- template method
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)   // subclass override --- fast path attempt
        doAcquireShared(arg);         // failed → enqueue as SHARED node, park
}

readLock.lock() delegates to AQS.acquireShared(1), which calls tryAcquireShared (the fast path). If that returns < 0 (failed), AQS calls doAcquireShared --- which does NOT park immediately. It retries first:

doAcquireShared --- Enqueue and Spin/Park Loop

java 复制代码
private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);  // enqueue as SHARED node
    for (;;) {
        final Node p = node.predecessor();
        if (p == head) {
            int r = tryAcquireShared(arg);     // retry BEFORE parking
            if (r >= 0) {
                setHeadAndPropagate(node, r);  // success → become head, wake next SHARED
                return;
            }
        }
        if (shouldParkAfterFailedAcquire(p, node) &&  // set predecessor waitStatus to SIGNAL
            parkAndCheckInterrupt())                    // LockSupport.park() ← PARK HERE
            selfInterrupt();
        // wake up → loop back → retry tryAcquireShared
    }
}

The sequence is: try → set SIGNAL → try again → park → wake → try again. At least two tryAcquireShared attempts before parking (when the node is right behind head):

复制代码
Iteration 1:
  predecessor == head → tryAcquireShared() → fails
  shouldParkAfterFailedAcquire() → sets predecessor waitStatus to SIGNAL
    → returns false (first time --- just set the flag, don't park yet)

Iteration 2:
  predecessor == head → tryAcquireShared() → still fails
  shouldParkAfterFailedAcquire() → predecessor already SIGNAL
    → returns true → parkAndCheckInterrupt() → PARK

... woken by unparkSuccessor ...

Iteration 3:
  predecessor == head → tryAcquireShared() → success!
  → setHeadAndPropagate → return

The double-check avoids unnecessary parking when the lock became available between enqueue and the first retry. On success, setHeadAndPropagate wakes the next SHARED node in the queue (propagation cascade --- same mechanism as CountDownLatch).

Note on exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current: getExclusiveOwnerThread() can never be null when exclusiveCount(c) != 0 under correct usage --- the owner is always set before the write count becomes non-zero, and cleared after it reaches zero. The two fields are updated together during acquire (CAS state then setExclusiveOwnerThread) and release (setExclusiveOwnerThread(null) then setState). So the null != current case is impossible in practice.

tryAcquireShared --- Fast Path

java 复制代码
protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();

    // Step 1: If write lock held by ANOTHER thread → fail immediately
    if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
        return -1;

    int r = sharedCount(c);

    // Step 2: Fair check + count check + CAS
    if (!readerShouldBlock() &&    // fair mode: no writer queued ahead
        r < MAX_COUNT &&            // haven't hit 65535 readers
        compareAndSetState(c, c + SHARED_UNIT))  // CAS upper 16 bits
    {
        // Step 3: Track per-thread 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();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;  // success
    }

    // Step 4: CAS failed or blocked → slow path
    return fullTryAcquireShared(current);
}

Step-by-step:

Step 1 --- Write lock check: If the write lock is held by another thread, the read lock must fail. But if the current thread holds the write lock, the read lock succeeds --- this is the lock downgrade path. The current thread already has exclusive access, so acquiring a read lock is safe.

Step 2 --- Three conditions for fast-path success:

  • !readerShouldBlock() --- In fair mode, checks if a writer is queued ahead. In non-fair mode, checks if the head of the sync queue is an exclusive (writer) waiter (to prevent writer starvation).
  • r < MAX_COUNT --- Haven't exceeded 65535 readers.
  • CAS succeeds --- Atomically increment the upper 16 bits.

All three must pass. If any fails, fall through to the slow path.

Step 3 --- Per-thread hold count tracking: The upper 16 bits only track the total reader count. But each thread needs its own hold count for reentrancy (so unlock() knows when to actually release). This uses a three-tier optimization.

Step 4 --- Slow path: If the CAS failed (contention) or readerShouldBlock() returned true, delegate to fullTryAcquireShared() which retries in a loop.

Per-Thread Read Hold Count --- Three-Tier Optimization

java 复制代码
static final class HoldCounter {
    int count = 0;
    final long tid = getThreadId(Thread.currentThread());
}

private transient HoldCounter cachedHoldCounter;  // last thread's counter
private transient Thread firstReader;              // first reader thread
private transient int firstReaderHoldCount;        // first reader's count

The upper 16 bits of state track total readers, but each thread needs its own count for reentrancy. ThreadLocal lookup is expensive, so three tiers optimize the common cases:

Tier Field When used Cost
1 firstReader + firstReaderHoldCount First thread to acquire read lock (most common: single reader) Direct field access
2 cachedHoldCounter Last thread that acquired read lock Direct field access
3 readHolds (ThreadLocal) All other threads ThreadLocal lookup

In the common case of a single reader or the same thread re-acquiring, tiers 1 and 2 avoid the ThreadLocal entirely.

What is ThreadLocal? ThreadLocal gives each thread its own private copy of a variable --- no locks, no CAS, no visibility issues. Each Thread object has a ThreadLocalMap (a hash map keyed by ThreadLocal instances). threadLocal.get() looks up the value in the current thread's map. The downside is the hash lookup cost (~20-50 ns), which is why the three-tier optimization exists.

Tier 1 --- firstReader (Single Reader Fast Path)

The very first thread to acquire the read lock (when sharedCount(state) goes from 0 → 1) gets stored directly in plain fields:

java 复制代码
if (r == 0) {
    firstReader = current;
    firstReaderHoldCount = 1;
} else if (firstReader == current) {
    firstReaderHoldCount++;
}

No ThreadLocal, no hash lookup --- just a direct field read. This optimizes the single-reader case, which is extremely common: one thread holds the read lock, does its work, releases. If the same thread re-acquires later (and no one else held it in between), it hits tier 1 again.

复制代码
Thread-A: readLock.lock()  → sharedCount was 0 → firstReader = Thread-A, count = 1
Thread-A: readLock.lock()  → firstReader == current → count = 2
Thread-A: readLock.unlock() → count = 1
Thread-A: readLock.unlock() → count = 0 → firstReader = null

Tier 2 --- cachedHoldCounter (Last Reader Cache)

Caches the HoldCounter of the last thread that acquired the read lock. When multiple readers are active, the most recent one is likely to call lock()/unlock() again soon (temporal locality):

java 复制代码
} else {
    HoldCounter rh = cachedHoldCounter;
    if (rh == null || rh.tid != getThreadId(current))
        cachedHoldCounter = rh = readHolds.get();  // cache miss → tier 3
    else if (rh.count == 0)
        readHolds.set(rh);  // re-install after previous cleanup
    rh.count++;
}
复制代码
Thread-A: readLock.lock()  → firstReader = Thread-A (tier 1)
Thread-B: readLock.lock()  → cachedHoldCounter = B's HoldCounter (tier 2/3 first time)
Thread-C: readLock.lock()  → cachedHoldCounter = C's HoldCounter
Thread-C: readLock.lock()  → cachedHoldCounter.tid == C → hit! count++ (tier 2)

Tier 3 --- ThreadLocal (Fallback)

For all other threads --- neither the first reader nor the most recent one. Full ThreadLocal.get() hash lookup:

复制代码
Thread-A: firstReader (tier 1)
Thread-C: cachedHoldCounter (tier 2)
Thread-B: readHolds.get() (tier 3) --- ThreadLocal lookup

Shared Object Reference --- cachedHoldCounter and ThreadLocal

cachedHoldCounter and the ThreadLocal entry point to the same HoldCounter object in memory. After cachedHoldCounter = rh = readHolds.get():

复制代码
Heap:
  HoldCounter @ 0xABC  { tid = B, count = 1 }
       ↑                       ↑
       │                       │
  cachedHoldCounter ──────────┘
       │
  Thread-B's ThreadLocalMap:
    readHolds → 0xABC ────────┘

One object, two references. When rh.count++ is called via either path, both see the same count.

The rh.count == 0 Edge Case --- Re-Install After Cleanup

When a thread fully releases the read lock, tryReleaseShared removes its ThreadLocal entry to avoid memory leaks:

java 复制代码
// In tryReleaseShared, when count reaches 0:
if (count <= 1) {
    readHolds.remove();  // cleanup ThreadLocal entry
}

But cachedHoldCounter still holds a reference to the old HoldCounter object (it wasn't cleared from the cache). If the same thread acquires the read lock again later:

  1. cachedHoldCounter.tid == current → tier 2 hit
  2. rh.count == 0 → the ThreadLocal was removed, but the object still exists via cache
  3. readHolds.set(rh) → re-install the same object back into the ThreadLocal
  4. rh.count++ → count goes from 0 to 1

Without readHolds.set(rh), the ThreadLocal would be empty. When the thread later calls unlock(), readHolds.get() would create a new HoldCounter with count = 0 instead of finding the one with count = 1 --- leading to an unmatched unlock exception.

复制代码
Timeline:
1. Thread-B: readLock.lock()  → cachedHoldCounter = rh(tid=B, count=1)
                                 readHolds[B] = rh  (same object)
2. Thread-B: readLock.unlock() → rh.count = 0 → readHolds.remove(B)  ← cleanup
                                 cachedHoldCounter still → rh  ← NOT cleared
3. Thread-B: readLock.lock()  → cachedHoldCounter.tid == B ✓
                                 rh.count == 0
                                 → readHolds.set(rh)  ← re-install into ThreadLocal
                                 → rh.count++ → count = 1

Performance Summary

Tier Lookup cost When hit
1 (firstReader) Field read --- ~1 ns Single reader, or first reader re-acquiring
2 (cachedHoldCounter) Field read + long compare --- ~2 ns Last reader re-acquiring
3 (ThreadLocal) Hash lookup --- ~20-50 ns All other threads

In a read-heavy workload with 1-2 dominant readers, tiers 1 and 2 handle the vast majority of calls. The ThreadLocal fallback is rarely hit.

fullTryAcquireShared --- Slow Path

When the fast path fails (CAS contention or readerShouldBlock()), this method retries in a loop:

java 复制代码
final int fullTryAcquireShared(Thread current) {
    HoldCounter rh = null;
    for (;;) {
        int c = getState();

        // Write lock held by another thread → fail
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
            // else: current thread holds write lock → allow (downgrade)
        }

        // Fair check: should this reader block?
        else if (readerShouldBlock()) {
            // Only allow if this thread already holds a read lock (reentrant)
            // First-time readers must queue
            if (firstReader == current) {
                // already holds read lock --- reentrant, proceed
            } else {
                if (rh == null) {
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current)) {
                        rh = readHolds.get();
                        if (rh.count == 0)
                            readHolds.remove();  // cleanup unused ThreadLocal
                    }
                }
                if (rh.count == 0)
                    return -1;  // first-time reader, must queue behind writer
            }
        }

        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");

        // CAS retry
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            // ... update hold counts (same as fast path) ...
            return 1;
        }
        // CAS failed → loop and retry
    }
}

Key insight in the slow path: When readerShouldBlock() returns true, the else if block acts as a filter , not a gate. It only blocks first-time readers (rh.count == 0 → return -1). Reentrant readers (rh.count > 0 or firstReader == current) pass through and reach the CAS at the bottom.

This is critical to prevent deadlock:

复制代码
Thread-A: holds read lock (count=1)
Writer:   queued, waiting for Thread-A to release
Thread-A: readLock.lock() again (reentrant)
  → readerShouldBlock() → true (writer is queued)
  → if we blocked Thread-A here → Thread-A parks behind the writer
  → writer waits for Thread-A to release read lock
  → Thread-A waits for writer to finish
  → DEADLOCK

So the rule is: readerShouldBlock() == true means "first-time readers must queue behind the writer." Reentrant readers are always allowed through. See [Fair vs Non-Fair](#Fair vs Non-Fair) for what readerShouldBlock() checks in each mode (non-fair: writer at head of queue; fair: anyone queued ahead).


Write Lock Deep Dive

The write lock uses AQS exclusive mode. Only one thread can hold it.

WriteLock.lock() --- Entry Point

java 复制代码
// WriteLock.lock() → sync.acquire(1)
// AQS.acquire --- template method
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&                              // subclass override --- fast path
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))   // failed → enqueue as EXCLUSIVE, park
        selfInterrupt();                                  // restore interrupt flag if interrupted
}

writeLock.lock() delegates to AQS.acquire(1), which calls tryAcquire (the fast path). If that returns false, AQS creates an EXCLUSIVE node, appends it to the sync queue, and enters the acquireQueued spin/park loop. When woken, it retries tryAcquire --- only one exclusive thread can succeed.

tryAcquire

java 复制代码
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c);

    if (c != 0) {
        // Step 1: state is non-zero --- someone holds a lock
        // If w == 0: read locks are held (c != 0 but no write count)
        //   → fail --- can't acquire write lock while readers are active
        // If w != 0 but different thread: another writer holds it
        //   → fail
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;

        // Step 2: Reentrant write --- same thread already holds write lock
        if (w + acquires > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        setState(c + acquires);  // no CAS needed --- we already hold the lock
        return true;
    }

    // Step 3: state == 0 --- no locks held at all
    if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

Step-by-step:

Step 1 --- c != 0 with w == 0: This is the critical check. c != 0 means some lock is held. w == 0 means no write lock --- so it must be read locks (upper bits are non-zero). Write lock always fails if any read locks are held, even by the current thread. This is why read-to-write upgrade is impossible.

Step 1 --- c != 0 with w != 0 and different owner: Another thread holds the write lock. Fail.

Step 2 --- Reentrant write: The current thread already holds the write lock. Just increment the count. No CAS needed because we already have exclusive access --- no other thread can be modifying state concurrently.

Step 3 --- c == 0: No locks held at all. Check fair mode (writerShouldBlock()), then CAS to acquire. Set the exclusive owner thread on success.

Reentrancy --- Same Lock Type, Multiple Acquires

Both read and write locks are reentrant --- the same thread can acquire the same lock type multiple times without unlocking first:

java 复制代码
// Read lock --- reentrant
rwLock.readLock().lock();   // read count: 0→1, hold count: 0→1
rwLock.readLock().lock();   // read count: 1→2, hold count: 1→2  ← no deadlock
rwLock.readLock().unlock(); // read count: 2→1, hold count: 2→1
rwLock.readLock().unlock(); // read count: 1→0, hold count: 1→0  ← fully released

// Write lock --- reentrant
rwLock.writeLock().lock();   // write count: 0→1, owner = me
rwLock.writeLock().lock();   // write count: 1→2, owner == me → just increment
rwLock.writeLock().unlock(); // write count: 2→1
rwLock.writeLock().unlock(); // write count: 1→0 → fully released

Write lock reentrancy works because tryAcquire checks ownership --- if current == getExclusiveOwnerThread(), it just increments state without CAS (no contention possible since we already hold exclusive access).

Read lock reentrancy works because tryAcquireShared increments the shared count and the per-thread hold count. The per-thread hold count tracks how many times this specific thread acquired, so each unlock() decrements by 1 and only the final unlock() actually releases.

Reentrancy vs Upgrade --- The Key Distinction

Same lock type (reentrant) Cross lock type
Read → Read ✅ Just increment count ---
Write → Write ✅ Owner check, increment ---
Write → Read ✅ Downgrade (safe) ---
Read → Write Deadlock tryAcquire fails on c != 0 && w == 0

Reentrancy is "I already have this type of access, give me more of the same." Upgrade is "I have shared access, give me exclusive access" --- fundamentally different because it requires kicking out all other readers.

No Lock Upgrade (Read → Write) --- Why It Deadlocks

Upgrading from read to write is NOT supported. tryAcquire explicitly fails when c != 0 && w == 0 (read locks held), even if the current thread holds those read locks.

Even a single thread deadlocks itself:

java 复制代码
rwLock.readLock().lock();     // state = 0x0001_0000 (read=1)
rwLock.writeLock().lock();    // tryAcquire: c != 0, w == 0 → FAIL
                               // → enqueue → park → DEADLOCK
                               // thread waits for read lock to be released,
                               // but it's the one holding it

With two threads it's even clearer:

复制代码
Thread-0: holds read lock → tries write lock → BLOCKED (Thread-1 holds read)
Thread-1: holds read lock → tries write lock → BLOCKED (Thread-0 holds read)
→ DEADLOCK --- each waits for the other to release read lock

The safe pattern is release-then-acquire, but there's a visibility gap:

java 复制代码
rwLock.readLock().unlock();   // release read first
// GAP: another writer could modify data here!
rwLock.writeLock().lock();    // now acquire write --- no deadlock

For atomic upgrade without the gap, use StampedLock.tryConvertToWriteLock().


Lock Release

ReadLock.unlock() --- Entry Point

java 复制代码
// ReadLock.unlock() → sync.releaseShared(1)
// AQS.releaseShared --- template method
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {   // subclass override --- decrement state
        doReleaseShared();          // state reached 0 → wake next thread in queue
        return true;
    }
    return false;                   // other readers still hold --- don't wake anyone
}

readLock.unlock() calls tryReleaseShared. If that returns true (all locks released, state == 0), AQS calls doReleaseShared() to wake the next queued thread. If other readers still hold, returns false --- no one is woken.

tryReleaseShared --- Read Lock Release

java 复制代码
protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();

    // Step 1: Decrement per-thread hold count
    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();  // cleanup ThreadLocal
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        --rh.count;
    }

    // Step 2: CAS decrement the shared count (upper 16 bits)
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            return nextc == 0;  // return true if ALL locks released (wake writer)
    }
}

Key points:

  • Step 1 decrements the per-thread hold count (three-tier: firstReader → cached → ThreadLocal)
  • Step 2 CAS-decrements the upper 16 bits in a retry loop
  • Returns true only when nextc == 0 --- meaning ALL locks (read and write) are released. This is when a queued writer should be woken up. If other readers still hold the lock, return false --- don't wake the writer yet.

WriteLock.unlock() --- Entry Point

java 复制代码
// WriteLock.unlock() → sync.release(1)
// AQS.release --- template method
public final boolean release(int arg) {
    if (tryRelease(arg)) {          // subclass override --- decrement state
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);      // wake the next thread in queue
        return true;
    }
    return false;                    // reentrant --- write count > 0, don't wake anyone
}

writeLock.unlock() calls tryRelease. If that returns true (write count reached 0), AQS wakes the next queued thread via unparkSuccessor. If the writer re-entered multiple times, returns false until the count reaches 0.

tryRelease --- Write Lock Release

java 复制代码
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);  // no CAS --- we hold exclusive lock
    return free;
}

Key points:

  • No CAS needed --- the current thread holds the exclusive lock, so no contention on state
  • Returns true only when the write hold count reaches 0 (fully released)
  • Clears the exclusive owner thread when fully released
  • If the writer re-entered multiple times, each unlock() decrements by 1. Only the final unlock() actually releases

Wake-Up After Release

When tryReleaseShared returns true (all readers released) or tryRelease returns true (writer released), AQS calls doReleaseShared() or unparkSuccessor() to wake the next thread in the sync queue:

  • Writer released → readers queued: All queued readers wake up via shared-mode propagation cascade (like CountDownLatch)
  • Writer released → writer queued: Next writer wakes up exclusively
  • All readers released → writer queued: The queued writer wakes up
  • Some readers released → writer queued: Writer stays parked --- tryReleaseShared returned false because nextc != 0 (other readers still hold)

The Sync Queue --- Readers and Writers Interleaved

All readers and writers share one single sync queue . Each node is tagged with its mode (Node.SHARED or Node.EXCLUSIVE):

复制代码
readLock.lock()  → addWaiter(Node.SHARED)      // shared node
writeLock.lock() → addWaiter(Node.EXCLUSIVE)    // exclusive node

Example queue with mixed readers and writers:

复制代码
Active readers: R1, R2 (acquired successfully, NOT in queue)

Sync queue:
head (dummy) → [W1 EXCLUSIVE] → [R3 SHARED] → [R4 SHARED] → [W2 EXCLUSIVE] ← tail
                  (parked)        (parked)       (parked)        (parked)

The node mode determines wake-up behavior --- shared nodes trigger propagation cascade, exclusive nodes stop it:

复制代码
All active readers release → wake W1 (exclusive --- one thread only)
W1 finishes → wake R3 (shared → propagate → also wake R4)
  R3 and R4 run concurrently
  Propagation stops at W2 (exclusive node blocks cascade)
R3 and R4 finish → wake W2

Readers behind a writer must wait for the writer. Even though readers can normally run concurrently, FIFO ordering means they queue behind the writer. This prevents writer starvation --- if readers could skip ahead of writers, writers might never get a turn.

Downgrade and the Sync Queue --- Spurious Wake-Up

When a thread downgrades (write → read) and a writer is queued, the write release triggers a spurious wake-up:

复制代码
Thread-0: writeLock.lock()   → state = 0x0000_0001
W2 arrives: tryAcquire fails → enqueue → park
Queue: head → [W2 EXCLUSIVE parked]

Thread-0: readLock.lock()    → state = 0x0001_0001 (downgrade)
Thread-0: writeLock.unlock() → state = 0x0001_0000
  → tryRelease returns true → unparkSuccessor → wake W2

W2 wakes: tryAcquire → c = 0x0001_0000, c != 0, w == 0 → FAIL
  → Thread-0 still holds read lock!
  → park again (spurious wake-up --- woken but can't acquire)

Thread-0: readLock.unlock() → state = 0x0000_0000 → return true
  → unparkSuccessor → wake W2 again

W2 wakes: tryAcquire → state == 0 → SUCCESS

The queued writer gets woken twice --- once prematurely (can't acquire because the downgraded reader still holds), once for real. This is a minor inefficiency of the downgrade pattern. The downgraded reader effectively blocks the entire queue: the writer waits for it, and any readers behind the writer wait for the writer.


Read vs Write Path --- Key Differences

Write Lock Read Lock
AQS mode Exclusive (acquire/release) Shared (acquireShared/releaseShared)
Node type Node.EXCLUSIVE Node.SHARED
State bits Lower 16 (write count) Upper 16 (read count)
CAS on acquire CAS(state, 0, 1) CAS(state, c, c + SHARED_UNIT)
CAS on release No CAS (already exclusive) CAS loop (concurrent readers)
Owner tracking setExclusiveOwnerThread Per-thread hold count (3-tier)
Wake-up on release unparkSuccessor (one thread) doReleaseShared (may propagate)
Propagation No Yes --- setHeadAndPropagate wakes next SHARED nodes
Return value of try* boolean (true/false) int (≥0 = success, <0 = fail)

Lock Downgrade (Write → Read)

You CAN downgrade from write to read without releasing --- this is safe and explicitly supported:

java 复制代码
rwLock.writeLock().lock();
try {
    modifyData();
    // Downgrade: acquire read lock while holding write lock
    rwLock.readLock().lock();
} finally {
    rwLock.writeLock().unlock();  // release write, keep read
}
try {
    // Now holding only read lock --- other readers can enter
    readData();
} finally {
    rwLock.readLock().unlock();
}

Why Downgrade Is Safe

The writer already has exclusive access --- no other thread can hold any lock. Acquiring the read lock just increments the upper 16 bits via CAS. The state transitions:

复制代码
Step 1: Writer holds lock
  state = 0x0000_0001  (read=0, write=1)

Step 2: Writer acquires read lock (downgrade)
  state = 0x0001_0001  (read=1, write=1)
  → tryAcquireShared succeeds because getExclusiveOwnerThread() == current

Step 3: Writer releases write lock
  state = 0x0001_0000  (read=1, write=0)
  → Other readers can now enter
  → Writer's modifications are visible (happens-before from write unlock)

Step 4: Thread releases read lock
  state = 0x0000_0000  (read=0, write=0)

Why Downgrade Matters

Without downgrade, there's a visibility gap:

java 复制代码
// WITHOUT downgrade --- dangerous gap
rwLock.writeLock().lock();
modifyData();
rwLock.writeLock().unlock();
// GAP: another writer could modify data here!
rwLock.readLock().lock();
readData();  // might see someone else's modifications
rwLock.readLock().unlock();

With downgrade, the thread transitions atomically from exclusive to shared access --- no gap for another writer to sneak in.

Real-World Example --- Cache Update with Downgrade

java 复制代码
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
Map<String, Object> cache = new HashMap<>();

public void updateAndRead(String key, Object newValue) {
    rwLock.writeLock().lock();          // state = 0x0000_0001 (write=1)
    try {
        cache.put(key, newValue);       // exclusive write --- no other thread can read or write

        rwLock.readLock().lock();       // state = 0x0001_0001 (read=1, write=1) --- downgrade
    } finally {
        rwLock.writeLock().unlock();    // state = 0x0001_0000 (read=1, write=0)
        // NOW other readers can enter --- but no writer can sneak in
    }

    try {
        Object val = cache.get(key);    // holding read lock --- guaranteed to see our own write
        process(val);                   // other readers can also read concurrently
    } finally {
        rwLock.readLock().unlock();     // state = 0x0000_0000 --- fully released
    }
}

Timeline --- Downgrade With Concurrent Readers

复制代码
Thread-0 (writer):
  writeLock.lock()     → state = 0x0000_0001
  modifyData()
  readLock.lock()      → state = 0x0001_0001  (downgrade)
  writeLock.unlock()   → state = 0x0001_0000  (other readers can enter now)

Thread-1 (reader):
  readLock.lock()      → state = 0x0002_0000  (2 readers)
  readData()           → sees Thread-0's modifications (happens-before)

Thread-0:
  readData()
  readLock.unlock()    → state = 0x0001_0000

Thread-1:
  readLock.unlock()    → state = 0x0000_0000

Fair vs Non-Fair

Non-Fair Mode (Default)

java 复制代码
new ReentrantReadWriteLock();  // non-fair by default

writerShouldBlock() --- always returns false. Writers never voluntarily yield --- they try to CAS immediately. This gives maximum throughput but can starve writers if readers keep arriving.

readerShouldBlock() --- returns true if the first queued thread is a writer (exclusive waiter). This is the "apparent first-queued" heuristic:

java 复制代码
// Non-fair Sync
final boolean readerShouldBlock() {
    return apparentlyFirstQueuedIsExclusive();
}

This prevents writer starvation in non-fair mode: if a writer is waiting at the head of the queue, new readers don't barge in --- they queue behind the writer. Without this, a continuous stream of readers could starve the writer indefinitely.

But it's not perfect: If the first queued thread is a reader, new readers CAN barge in ahead of it. Only a writer at the head triggers blocking.

Reader barging timeline --- when it happens:

复制代码
Thread-W: holds write lock
Queue: head → [R1 SHARED parked]

Thread-W: writeLock.unlock() → state = 0 → unparkSuccessor → wake R1
  R1 is waking up but hasn't re-acquired yet (still in acquireQueued spin loop)...

Thread-R2 arrives RIGHT NOW: readLock.lock()
  → tryAcquireShared:
    → exclusiveCount == 0 (writer released)
    → readerShouldBlock? → apparentlyFirstQueuedIsExclusive()
    → head.next = R1 → R1 is SHARED, not EXCLUSIVE
    → return false → reader should NOT block
    → CAS(state, 0, SHARED_UNIT) → SUCCESS
    → R2 barges in ahead of R1!

R1 finally runs: tryAcquireShared → also succeeds (shared mode, both can read)

R2 acquired the read lock before R1, even though R1 was queued first. This is fine for correctness (both are readers, they run concurrently), but R1 waited longer and got served later --- unfair. This is a deliberate trade-off: barging improves throughput (R2 doesn't need to enqueue/park/wake), at the cost of strict FIFO ordering among readers.

If the head were a writer instead, R2 would be blocked:

复制代码
Queue: head → [W2 EXCLUSIVE]
R2 arrives: readerShouldBlock? → head.next is EXCLUSIVE → YES → must queue

Fair Mode

java 复制代码
new ReentrantReadWriteLock(true);  // fair

Both readerShouldBlock() and writerShouldBlock() check hasQueuedPredecessors() --- if any thread is queued ahead, the current thread must queue too. Strict FIFO ordering.

java 复制代码
// Fair Sync
final boolean readerShouldBlock() {
    return hasQueuedPredecessors();
}
final boolean writerShouldBlock() {
    return hasQueuedPredecessors();
}

Trade-off: Fair mode prevents starvation entirely but reduces throughput. In non-fair mode, a reader can acquire the lock immediately if it's available (no queue check). In fair mode, it must check the queue first --- even if the lock is free, it queues behind earlier arrivals.

Writer Starvation --- The Problem

With many readers in non-fair mode, a writer may wait indefinitely:

复制代码
Time 0: Reader-1 holds read lock
Time 1: Writer arrives → waits (readers active)
Time 2: Reader-2 arrives → acquires read lock (barges in --- writer not at head yet)
Time 3: Reader-1 releases → Reader-2 still holds → Writer still waits
Time 4: Reader-3 arrives → sees writer at head → queues behind writer ✓
Time 5: Reader-2 releases → Writer finally acquires

The non-fair readerShouldBlock() heuristic (check if head is a writer) mitigates but doesn't eliminate starvation. For guaranteed fairness, use fair mode.


Memory Ordering and Happens-Before

ReentrantReadWriteLock provides the same memory visibility guarantees as synchronized. Understanding the mechanism helps reason about when data written under one lock is visible to threads acquiring another.

The Guarantee

The JMM (Java Memory Model) defines these happens-before edges for read-write locks:

复制代码
writeLock.unlock()  ──happens-before──►  writeLock.lock()   (by another thread)
writeLock.unlock()  ──happens-before──►  readLock.lock()    (by another thread)
readLock.unlock()   ──happens-before──►  writeLock.lock()   (by another thread)

Notice what's missing : readLock.unlock() does NOT happen-before readLock.lock() by another thread. Two concurrent readers don't synchronize with each other --- they don't need to, since neither is writing.

How It Works --- volatile State + CAS

AQS declares state as volatile. Every lock acquisition reads state (via getState() or CAS), and every release writes state (via setState() or CAS). The JMM guarantees:

  1. volatile write → volatile read creates a happens-before edge
  2. CAS has the memory effects of both a volatile read and a volatile write

So when Thread-A calls writeLock.unlock() (writes state), and Thread-B subsequently calls readLock.lock() (reads state via CAS), Thread-B is guaranteed to see all of Thread-A's writes --- not just to state, but to all variables written before the unlock. This is the "piggybacking" effect: the volatile write flushes the entire store buffer.

复制代码
Thread-A:                          Thread-B:
  data = 42;                         
  cache.put("key", value);           
  writeLock.unlock()                 
    → setState(0)  [volatile write]  
                                     readLock.lock()
                                       → getState() [volatile read]
                                       → sees state = 0
                                       → CAS(0, SHARED_UNIT) [volatile read+write]
                                     // Thread-B now sees data = 42 and cache updates

Why Write Lock Release Doesn't Need CAS

In tryRelease, the write lock uses setState(nextc) --- a plain volatile write, not CAS. This is safe because the current thread holds exclusive access. No other thread can be modifying state concurrently. The volatile write still provides the memory ordering guarantee (store buffer flush + happens-before edge).

In contrast, tryReleaseShared (read lock release) uses a CAS loop because multiple readers may be releasing concurrently --- they need atomic decrement.

Practical Implication --- Lock Downgrade Visibility

Lock downgrade preserves visibility without a gap:

复制代码
Thread-0:
  writeLock.lock()
  data = newValue;           // write under exclusive lock
  readLock.lock()            // downgrade --- CAS on state
  writeLock.unlock()         // volatile write to state
                             // happens-before any subsequent lock acquisition

Thread-1:
  readLock.lock()            // volatile read of state → sees Thread-0's writes
  use(data);                 // guaranteed to see newValue

Without downgrade, releasing the write lock and then acquiring the read lock leaves a window where another writer could intervene --- and that writer's modifications would be visible instead.


Condition Support

Only the write lock supports Condition:

java 复制代码
Condition condition = rwLock.writeLock().newCondition();

rwLock.writeLock().lock();
try {
    while (!conditionMet) {
        condition.await();  // releases write lock, parks
    }
    // condition met --- proceed with exclusive access
} finally {
    rwLock.writeLock().unlock();
}

The read lock does NOT support newCondition() --- calling it throws UnsupportedOperationException.

Why Read Lock Can't Have Conditions

Condition.await() must release the lock, park, then re-acquire. For the read lock, "release" means decrementing the shared count. But multiple threads hold the read lock simultaneously --- await() would only release ONE thread's hold, leaving others still holding it. When signal() tries to wake the thread, it needs to re-acquire the read lock, but the semantics of "wait until some condition is true" don't map cleanly to shared locks.

More fundamentally, conditions are about mutual exclusion: "I hold exclusive access, I'll release it and wait for a signal, then re-acquire exclusive access." The read lock is shared, not exclusive --- the condition pattern doesn't apply.


tryLock and Interruptible Acquisition

Beyond the standard lock(), both read and write locks support non-blocking and timed acquisition.

tryLock() --- Non-Blocking, Always Barges

java 复制代码
// Returns immediately --- never parks
if (rwLock.readLock().tryLock()) {
    try { /* read */ }
    finally { rwLock.readLock().unlock(); }
} else {
    // lock not available --- handle gracefully
}

tryLock() (no arguments) always barges --- it ignores fairness settings entirely. Even in fair mode, it skips the queue and attempts to CAS immediately. This is by design: tryLock() is for "try once, don't wait" semantics where FIFO ordering doesn't matter.

Internally, tryLock() on the read lock calls tryReadLock() (not tryAcquireShared), which skips the readerShouldBlock() check:

java 复制代码
// Simplified --- ReadLock.tryLock()
final boolean tryReadLock() {
    for (;;) {
        int c = getState();
        if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
            return false;  // another writer holds it --- fail
        int r = sharedCount(c);
        if (r == MAX_COUNT) throw new Error("Maximum lock count exceeded");
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            // update hold counts...
            return true;   // success --- no fairness check!
        }
        // CAS failed → retry
    }
}

tryLock(timeout) --- Timed, Respects Fairness

java 复制代码
if (rwLock.writeLock().tryLock(5, TimeUnit.SECONDS)) {
    try { /* write */ }
    finally { rwLock.writeLock().unlock(); }
} else {
    // timed out --- couldn't acquire within 5 seconds
}

Unlike tryLock(), the timed variant does respect fairness. It calls tryAcquireNanostryAcquire (which checks writerShouldBlock()). If it can't acquire immediately, it parks with a deadline using LockSupport.parkNanos().

lockInterruptibly() --- Responds to Interrupts

java 复制代码
try {
    rwLock.writeLock().lockInterruptibly();
    try { /* write */ }
    finally { rwLock.writeLock().unlock(); }
} catch (InterruptedException e) {
    // thread was interrupted while waiting for the lock
    Thread.currentThread().interrupt();  // restore interrupt flag
}

Standard lock() ignores interrupts --- it re-acquires the interrupt flag after waking but doesn't throw. lockInterruptibly() throws InterruptedException immediately when the thread is interrupted while waiting, allowing the caller to cancel the operation.

Comparison

Method Blocks? Respects fairness? Responds to interrupt?
lock() Yes (indefinitely) Yes No (deferred)
tryLock() No (returns immediately) No (always barges) No
tryLock(timeout) Yes (up to timeout) Yes Yes
lockInterruptibly() Yes (indefinitely) Yes Yes

Diagnostic and Monitoring APIs

ReentrantReadWriteLock exposes several methods useful for debugging, monitoring, and testing. These are not for control flow --- they return snapshots that may be stale by the time you act on them.

Hold Count Queries

java 复制代码
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

// How many times the CURRENT thread holds the read lock
int readHolds = rwLock.getReadHoldCount();    // per-thread, from the 3-tier system

// Total number of read lock acquisitions across ALL threads
int readCount = rwLock.getReadLockCount();     // upper 16 bits of state

// How many times the CURRENT thread holds the write lock
int writeHolds = rwLock.getWriteHoldCount();   // lower 16 bits of state

// Is the write lock held by ANY thread?
boolean writeLocked = rwLock.isWriteLocked();

// Is the write lock held by the CURRENT thread?
boolean heldByMe = rwLock.isWriteLockedByCurrentThread();

Key distinction: getReadHoldCount() returns the current thread's hold count (from the three-tier per-thread tracking). getReadLockCount() returns the total across all threads (from the upper 16 bits of state). These are different values when multiple readers hold the lock.

Queue Inspection

java 复制代码
// Are any threads waiting to acquire?
boolean hasWaiters = rwLock.hasQueuedThreads();

// Is a specific thread waiting?
boolean threadQueued = rwLock.hasQueuedThread(someThread);

// Approximate number of waiting threads
int queueLength = rwLock.getQueueLength();

// Snapshot of waiting threads (for debugging only)
Collection<Thread> waiters = rwLock.getQueuedThreads();  // protected method

Warning: These methods are inherently racy. By the time you read the queue length, threads may have acquired or given up. Use them for monitoring dashboards and debug logging, never for lock acquisition decisions.

Practical Use --- Debug Logging

java 复制代码
public void debugLockState(ReentrantReadWriteLock rwLock) {
    log.debug("Lock state: readers={}, writers={}, writeHeldByMe={}, queueLength={}",
        rwLock.getReadLockCount(),
        rwLock.isWriteLocked() ? rwLock.getWriteHoldCount() : 0,
        rwLock.isWriteLockedByCurrentThread(),
        rwLock.getQueueLength());
}

Common Pitfalls and Anti-Patterns

Beyond the well-known "no lock upgrade" issue, several subtler mistakes trip up developers.

Pitfall 1: Forgetting to Unlock in Exception Paths

java 复制代码
// WRONG --- if readData() throws, lock is never released
rwLock.readLock().lock();
Object data = readData();  // might throw!
rwLock.readLock().unlock();

// CORRECT --- always use try-finally
rwLock.readLock().lock();
try {
    Object data = readData();
} finally {
    rwLock.readLock().unlock();
}

This applies to all locks, but read-write locks make it worse: a leaked read lock silently blocks all future writers. The system appears to work (readers succeed) but writes hang forever. A leaked write lock is more obvious --- everything blocks.

Pitfall 2: Lock Ordering Violations Between Multiple RWLocks

java 复制代码
// Thread-1:                          Thread-2:
rwLock1.readLock().lock();            rwLock2.writeLock().lock();
rwLock2.writeLock().lock();  // WAIT  rwLock1.writeLock().lock();  // WAIT
// → DEADLOCK

When using multiple ReentrantReadWriteLock instances, establish a consistent acquisition order. This is the same principle as with ReentrantLock, but read-write locks make it easier to miss because read locks feel "safe."

Pitfall 3: Holding Locks During I/O or Long Operations

java 复制代码
// BAD --- holds read lock during network call
rwLock.readLock().lock();
try {
    Object data = cache.get(key);
    sendToRemoteService(data);  // network I/O --- could take seconds
} finally {
    rwLock.readLock().unlock();
}

// BETTER --- copy data, release lock, then do I/O
rwLock.readLock().lock();
Object dataCopy;
try {
    dataCopy = deepCopy(cache.get(key));
} finally {
    rwLock.readLock().unlock();
}
sendToRemoteService(dataCopy);  // no lock held

Long-held read locks starve writers. Copy what you need, release the lock, then do the slow work.

Pitfall 4: Using Read Lock for "Read-Then-Write" (Check-Then-Act)

java 复制代码
// WRONG --- race condition between check and update
rwLock.readLock().lock();
try {
    if (!cache.containsKey(key)) {
        rwLock.readLock().unlock();
        rwLock.writeLock().lock();
        try {
            cache.put(key, computeValue());  // another thread may have added it!
        } finally {
            rwLock.writeLock().unlock();
        }
        return;
    }
    return cache.get(key);
} finally {
    rwLock.readLock().unlock();  // double-unlock if we took the write path!
}

This has two bugs: (1) the check-then-act race --- another thread could insert the key between the read unlock and write lock, and (2) the finally block double-unlocks. The correct pattern uses double-checked locking under the write lock:

java 复制代码
rwLock.readLock().lock();
try {
    Object val = cache.get(key);
    if (val != null) return val;
} finally {
    rwLock.readLock().unlock();
}
// No lock held --- gap exists, but we double-check below
rwLock.writeLock().lock();
try {
    Object val = cache.get(key);       // double-check under write lock
    if (val != null) return val;
    val = computeValue();
    cache.put(key, val);
    return val;
} finally {
    rwLock.writeLock().unlock();
}

Pitfall 5: Assuming Read Lock Is Free

Read lock acquisition is not free --- it involves a CAS on the shared state field, which causes cache line bouncing under contention (see [Cache Line Bouncing](#Cache Line Bouncing)). If your read path is very short (a single field read), the lock overhead may exceed the protected operation. Consider volatile, AtomicReference, or StampedLock.tryOptimisticRead() for such cases.


Common Patterns

Cache with Read-Heavy Access

java 复制代码
class Cache<K, V> {
    private final Map<K, V> map = new HashMap<>();
    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    public V get(K key) {
        lock.readLock().lock();
        try { return map.get(key); }
        finally { lock.readLock().unlock(); }
    }

    public void put(K key, V value) {
        lock.writeLock().lock();
        try { map.put(key, value); }
        finally { lock.writeLock().unlock(); }
    }
}

Config Reload with Lock Downgrade

java 复制代码
class ConfigManager {
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private volatile Config config;

    public Config getConfig() {
        lock.readLock().lock();
        try {
            if (!config.isStale()) return config;  // fast path --- no reload needed
        } finally {
            lock.readLock().unlock();
        }

        // GAP: config could become non-stale here (another thread reloaded),
        // so we double-check under write lock below.
        lock.writeLock().lock();
        try {
            if (config.isStale()) {     // double-check under write lock
                config = loadConfig();
            }
            lock.readLock().lock();     // downgrade: acquire read before releasing write
        } finally {
            lock.writeLock().unlock();  // release write, keep read
        }

        try {
            return config;              // holding read lock --- guaranteed fresh
        } finally {
            lock.readLock().unlock();
        }
    }
}

Publish Pattern --- Write Then Allow Reads

java 复制代码
// Thread-0: prepare data exclusively, then let everyone read
rwLock.writeLock().lock();
try {
    prepareData();           // exclusive access
    rwLock.readLock().lock(); // downgrade
} finally {
    rwLock.writeLock().unlock();
}
// Now holding read lock --- other readers can see the prepared data
// No writer can sneak in between prepare and read
try {
    useData();
} finally {
    rwLock.readLock().unlock();
}

Performance Characteristics

When ReentrantReadWriteLock Wins

RRWL outperforms ReentrantLock when:

  • Read-to-write ratio is high (>10:1 or higher)
  • Read-side critical sections are non-trivial (enough work to amortize lock overhead)
  • Writer frequency is low (writers don't constantly invalidate reader concurrency)

The benefit comes from reader parallelism: N readers run concurrently instead of serialized. If each read takes 1ms and you have 8 threads, ReentrantLock serializes to 8ms total while RRWL completes in ~1ms.

When ReentrantReadWriteLock Loses

RRWL can be slower than ReentrantLock when:

  • Read-to-write ratio is low (<5:1) --- the overhead of packed state, per-thread hold counts, and ThreadLocal lookups exceeds the parallelism benefit
  • Read-side critical sections are trivial (a single field read) --- lock overhead dominates the actual work
  • High reader contention --- many readers CAS-ing the same state field causes cache line bouncing across cores

Cost Breakdown

Operation ReentrantLock RRWL Read Lock RRWL Write Lock
Uncontended acquire ~15 ns (1 CAS) ~25 ns (1 CAS + hold count) ~15 ns (1 CAS)
Uncontended release ~10 ns (volatile write) ~20 ns (CAS + hold count) ~10 ns (volatile write)
Contended acquire Park + wake (~10 μs) CAS retry loop, then park Park + wake (~10 μs)
Memory per thread None HoldCounter in ThreadLocal (~32 bytes) None

The read lock is inherently more expensive per-operation than a simple exclusive lock. The win comes from concurrency, not per-operation speed.

Cache Line Bouncing --- The Hidden Cost

Every read lock acquisition CAS-es the state field. CAS is a write operation --- the hardware cache coherence protocol (MESI on x86) requires exclusive ownership of the cache line to perform it. This invalidates the line on all other cores.

With 4 readers acquiring simultaneously:

复制代码
state lives at address 0x1000, in cache line #42 (64 bytes)

Core 0: CAS(state, 0, SHARED_UNIT)
  → Gets exclusive ownership of line #42
  → Invalidates line #42 on Cores 1, 2, 3
  → Succeeds → state = 0x0001_0000

Core 1: CAS(state, 0, SHARED_UNIT)
  → Cache miss! Line #42 was invalidated
  → Fetches from Core 0's cache (~30-50 ns penalty)
  → CAS fails (expected 0, found 0x0001_0000)
  → Retry: CAS(state, 0x0001_0000, 0x0002_0000)
  → Gets exclusive → invalidates Cores 0, 2, 3
  → Succeeds

Core 2: same pattern --- fetch, fail, retry, invalidate
Core 3: same pattern

The cache line ping-pongs between cores. Each successful CAS invalidates all other cores. The cost scales with core count:

Cores CAS behavior
2 Mild --- occasional retry
4 Noticeable --- 1-2 retries per CAS
8 Significant --- retry loops get longer
16+ Severe --- most CAS attempts fail, throughput degrades

Why read locks are worse than write locks for this: Write locks are exclusive --- only one thread CAS-es at a time. Read locks are concurrent --- N threads CAS the same field simultaneously during both lock() and unlock(), producing 2N CAS operations bouncing the same cache line.

Why StampedLock.tryOptimisticRead() avoids this: It uses a volatile read (no write), which doesn't invalidate the cache line. Multiple cores can read the same line simultaneously in MESI SHARED state --- no bouncing, no retries. The trade-off is that the optimistic read can fail if a writer intervened, requiring a fallback to a full lock.

Benchmarking Guidance

Before choosing RRWL, measure with your actual workload:

java 复制代码
// JMH benchmark skeleton
@State(Scope.Benchmark)
public class RWLockBenchmark {
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final ReentrantLock mutex = new ReentrantLock();
    private Map<String, Object> data = new HashMap<>();

    @Benchmark @Group("rwlock") @GroupThreads(7)
    public Object rwlockRead() {
        rwLock.readLock().lock();
        try { return data.get("key"); }
        finally { rwLock.readLock().unlock(); }
    }

    @Benchmark @Group("rwlock") @GroupThreads(1)
    public void rwlockWrite() {
        rwLock.writeLock().lock();
        try { data.put("key", new Object()); }
        finally { rwLock.writeLock().unlock(); }
    }

    @Benchmark @Group("mutex") @GroupThreads(7)
    public Object mutexRead() {
        mutex.lock();
        try { return data.get("key"); }
        finally { mutex.unlock(); }
    }

    @Benchmark @Group("mutex") @GroupThreads(1)
    public void mutexWrite() {
        mutex.lock();
        try { data.put("key", new Object()); }
        finally { mutex.unlock(); }
    }
}

What to vary: thread count, read/write ratio, critical section duration (add Blackhole.consumeCPU() to simulate work). The crossover point where RRWL beats ReentrantLock depends on all three.

Decision Heuristic

复制代码
Is the shared state a Map?
  → Yes → Use ConcurrentHashMap (faster than any lock)
  → No ↓

Read-to-write ratio > 10:1?
  → No → Use ReentrantLock (simpler, less overhead)
  → Yes ↓

Read critical section > 100 ns of work?
  → No → Consider StampedLock.tryOptimisticRead() or volatile/Atomic*
  → Yes ↓

Need reentrancy or Condition on write lock?
  → Yes → Use ReentrantReadWriteLock
  → No → Consider StampedLock (faster reads, but non-reentrant)

FAQ

Q: When should I use ReentrantReadWriteLock vs ReentrantLock?

When reads vastly outnumber writes (>10:1) and the read-side critical section does meaningful work. If reads and writes are roughly equal, the overhead of the read-write lock (packed state, per-thread hold counts, ThreadLocal) makes it slower than a simple ReentrantLock. See [Performance Characteristics](#Performance Characteristics) for the full decision heuristic and benchmarking guidance.

Q: When should I use ReentrantReadWriteLock vs ConcurrentHashMap?

If your shared data is a Map, use ConcurrentHashMap --- it's faster (lock striping, lock-free reads). ReentrantReadWriteLock is for protecting arbitrary shared state that isn't a map: complex objects, multiple fields, data structures without built-in concurrency.

Q: Can a thread hold both read and write locks?

Yes, but only write → read (downgrade). See [Reentrancy vs Upgrade](#Reentrancy vs Upgrade) and [Lock Downgrade](#Lock Downgrade).

Q: Is the read lock truly lock-free?

No. It uses CAS on the AQS state, which is lock-free in the sense of no blocking, but it's not wait-free --- CAS can fail under contention and retry. See [Cache Line Bouncing](#Cache Line Bouncing) for why this matters. For truly optimistic reads without any CAS, use StampedLock.tryOptimisticRead().

Q: What happens if I call readLock().unlock() without holding the read lock?

tryReleaseShared checks the per-thread hold count. If it's 0 or negative, it throws IllegalMonitorStateException ("attempt to unlock read lock, not locked by current thread").

Q: Why is MAX_COUNT 65535 and not higher?

Because the 32-bit state is split 16/16. See [Maximum Counts](#Maximum Counts) for the design trade-off analysis of alternative splits.

Q: Does the read lock provide happens-before guarantees?

Yes. See [Memory Ordering and Happens-Before](#Memory Ordering and Happens-Before) for the full explanation. In short: writeLock.unlock() happens-before a subsequent readLock.lock() by another thread, and readLock.unlock() happens-before a subsequent writeLock.lock(). But two concurrent readers do NOT synchronize with each other --- they don't need to.

Q: Can I use tryLock() with read-write locks?

Yes. Both readLock().tryLock() and writeLock().tryLock() are supported, with optional timeout. See [tryLock and Interruptible Acquisition](#tryLock and Interruptible Acquisition) for the full API comparison and barging semantics.

Q: Is ReentrantReadWriteLock serializable?

Yes --- it implements Serializable. But deserialization always creates an unlocked lock, regardless of the state when serialized. This is intentional: lock state is inherently tied to threads, which don't survive serialization. The same applies to ReentrantLock.

Q: Can I use ReentrantReadWriteLock across multiple JVMs?

No. Like all java.util.concurrent locks, it's a single-JVM, in-process synchronization primitive. For distributed locking, use external coordination services (ZooKeeper, Redis, DynamoDB conditional writes).

Q: What happens if a thread dies while holding a lock?

The lock is never released --- there's no automatic cleanup. Other threads waiting for the lock will block forever. This is why try-finally is critical. In server environments, consider using tryLock(timeout) to avoid indefinite blocking when a lock holder might crash.


Comparison with Other Locks

Feature ReentrantLock ReentrantReadWriteLock StampedLock
AQS-based Yes (exclusive) Yes (packed state) No (custom)
Concurrent readers No Yes Yes (optimistic + read lock)
Reentrant Yes Yes (both locks) No
Condition support Yes Write lock only No
Lock downgrade N/A Yes (write → read) Yes (write → read)
Lock upgrade N/A No Yes (tryConvertToWriteLock)
Fair mode Yes Yes No
Optimistic reads No No Yes (tryOptimisticRead)
Performance (read-heavy) Poor (serialized) Good Best (optimistic)
Complexity Low Medium High

When to Use Which

复制代码
ReentrantLock:
├── Reads and writes roughly equal
├── Need Condition support
├── Simple mutual exclusion
└── Don't want complexity of read-write semantics

ReentrantReadWriteLock:
├── Reads vastly outnumber writes
├── Need reentrant read AND write locks
├── Need Condition on write lock
├── Need lock downgrade (write → read)
└── Need fair mode option

StampedLock:
├── Extreme read-heavy workload
├── Can tolerate non-reentrant locks
├── Want optimistic reads (no CAS, no blocking)
├── Need lock upgrade (read → write)
└── Don't need Condition support

Key Takeaways

  1. One int, two locks. The packed 16/16 state design lets a single CAS atomically manage both read and write counts --- no external lock needed.

  2. Read lock is shared, not free. Every acquisition CAS-es the state field. Under high contention, cache line bouncing can make it slower than a simple mutex. Measure before assuming RRWL is faster.

  3. Downgrade yes, upgrade no. Write → read is safe (you already have exclusive access). Read → write deadlocks because you can't atomically kick out other readers.

  4. Three-tier hold count is a performance optimization, not a correctness requirement. firstReader and cachedHoldCounter exist solely to avoid ThreadLocal lookups in the common case.

  5. Non-fair mode prevents writer starvation via readerShouldBlock(). New readers yield to a queued writer at the head of the sync queue --- but only at the head.

  6. Memory visibility comes from volatile state + CAS. The same mechanism as synchronized, just implemented with CAS instead of monitor enter/exit.

  7. For extreme read-heavy workloads, consider StampedLock. Its optimistic read avoids CAS entirely --- just a volatile read + validation.


相关推荐
Morwit7 小时前
【力扣hot100】 494. 目标和
数据结构·算法·leetcode
handler017 小时前
算法:图的基本概念
c语言·开发语言·c++·笔记·算法·图论
科研前沿7 小时前
像素即坐标・室外无边界:2026 最新无感定位技术,驱动数字孪生实景可控—— 镜像视界技术白皮书
大数据·人工智能·算法·重构·空间计算
超级无敌葛大侠7 小时前
Redis里RDB和AOF的区别
java·redis
YJlio7 小时前
《Windows Internals》10.5.1 ETW 概述:看懂 Windows 的“事件高速公路”
java·windows·笔记·stm32·嵌入式硬件·学习·eclipse
少许极端7 小时前
算法奇妙屋(五十)-二分与双指针的结合 + 2024秦皇岛-Problem D
算法·二分+双指针
Victor3567 小时前
MongoDB(116)升级MongoDB时需要注意哪些事项?
后端
love在水一方7 小时前
【Voxel-SLAM】 体素地图与Bundle Adjustment算法深度分析(四)
人工智能·算法·机器学习
budingxiaomoli7 小时前
SpringCloud概述
java·spring cloud·微服务