Analysis of ReentrantReadWriteLock Principles and StampedLock Implementation
1. Overview
ReentrantReadWriteLock utilizes a single AQS synchronizer. The internal state (an integer state) is divided: the lower 16 bits track the exclusive writer lock hold count, while the upper 16 bits track the shared reader lock count. Two synchronizer implementations exist: NonfairSync and FairSync. Their primary distinction lies in the readerShouldBlock() and writerShouldBlock() methods, which control lock acquisition policy.
abstract static class Sync extends AbstractQueuedSynchronizer {
// State division constants
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;
// Methods to extract reader/writer counts
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
}
// Nonfair version
static final class NonfairSync extends Sync {
final boolean writerShouldBlock() { return false; } // Writers can always barge
final boolean readerShouldBlock() {
// Heuristic: block if head of queue appears to be a waiting writer
return apparentlyFirstQueuedIsExclusive();
}
}
// Fair version
static final class FairSync extends Sync {
final boolean writerShouldBlock() { return hasQueuedPredecessors(); }
final boolean readerShouldBlock() { return hasQueuedPredecessors(); }
}
2. Write Lock Acquisition and Reader Blocking
Scenario: Thread T1 holds write lock, Thread T2 attempts read lock.
Step 1: Write Lock Acquisition Success
When a writer thread (T1) calls writeLock.lock(), it invokes the AQS acquire(1) method. The tryAcquire method in Sync checks if the lock is free. If state == 0, it sets the exclusive owner thread and increments the write lock count via CAS. If successful, T1 proceeds.
Step 2: Read Lock Acquisition Failure
A reader thread (T2) calls readLock.lock(), which invokes acquireShared(1). The tryAcquireShared method checks the lock state. If an exclusive write lock is held (exclusiveCount(c) != 0), and the holder is not the current thread, it returns -1, indicating failure. The thread then enters the doAcquireShared method, is wrapped in a Node with SHARED mode, and added to the CLH queue. After checking if it's at the head of the queue and failing to acquire again, it parks itself.
Step 3: Write Lock Release and Reader Wake-up
When T1 releases the write lock via unlock(), sync.release(1) is called. tryRelease decrements the write lock count. If the count reaches zero, the exclusive owner is cleared. Subsequently, unparkSuccessor is invoked on the queue head, unparking the next waiting thread (T2).
T2 resumes execution in doAcquireShared. Its loop continues, and this time tryAcquireShared succeeds because no write lock is held. The reader count (upper 16 bits of state) is incremented via CAS. setHeadAndPropagate is then called. This method sets T2's node as the new head and, crucially, checks if propagation should continue. Since T2 acquired a shared (read) lock and its successor node (T3) is also in SHARED mode, doReleaseShared is called, which unpacks T3. This creates a chain reaction, waking all consecutive reader threads in the queue.
3. StampedLock: Optimistic Reads and Application
Purpose
ReentrantReadWriteLock requires CAS operations on the state even for concurrent reads. StampedLock optimizes this further with an optimistic read mechanism. Unlike ReentrantReadWriteLock, StampedLock does not support condition variables or reentrancy.
Key Feature: Optimistic Reading
An optimistic read is essentially a lock-free read operation. The tryOptimisticRead() method returns a stamp. After performing the read, the stamp must be validated using validate(stamp). If valid, no write occurred during the read, and the data is safe. If invalid, the read must be retried under a pessimistic (full) read lock—a form of lock upgrade.
Usage Example
import java.util.concurrent.locks.StampedLock;
class DataContainer {
private int data;
private final StampedLock lock = new StampedLock();
public int read(int readTimeMs) {
// 1. Attempt optimistic read
long stamp = lock.tryOptimisticRead();
int localData = data; // Perform read
// Simulate read work
try { Thread.sleep(readTimeMs); } catch (InterruptedException e) {}
// 2. Validate stamp
if (lock.validate(stamp)) {
return localData; // Data is consistent
}
// 3. Stamp invalid: upgrade to full read lock
stamp = lock.readLock();
try {
localData = data;
try { Thread.sleep(readTimeMs); } catch (InterruptedException e) {}
return localData;
} finally {
lock.unlockRead(stamp);
}
}
public void write(int newValue) {
long stamp = lock.writeLock();
try {
data = newValue;
} finally {
lock.unlockWrite(stamp);
}
}
}
Concurrent Read Test: When multiple threads perform optimistic reads and no write intervenes, all reads complete without acquiring a lock, as shown by consistent stamp values in logs.
Concurrent Read-Write Test: If a write occurs during an optimistic read, the stamp validation fails. The reading thread then upgrades to a full read lock, ensuring data consistency before returning the value.