preface
After looking at ReentrantLock, ReentrantLock is sufficient in high concurrency scenarios, but because ReentrantLock is an exclusive lock, and only one thread can acquire the lock, and many scenarios are read more than write less, This is not a good time to use ReentrantLock. How to use the scenario of reading too much and writing too little? The JUC package also provides a read and write lock, ReentrantReadWriteLock, to handle the scenario where there are too many reads and too few writes.
[liuzhirichard] Record technology, development and source code notes in work and study. From time to time, share what you’ve seen and heard in your life. Welcome to guide!
introduce
Support ReadWriteLock implementation with reentrantLock-like semantics.
Has the following properties:
- To get the order
This class does not impose read – or write-first ordering on lock access. It does, however, support alternative fairness strategies.
Fair mode and unfair mode are supported. The default mode is unfair.
- reentrant
Allows readers and writers to reacquire read or write locks in a ReentrantLock style. The reader does not allow reentrant use of any write locks held by the writer thread until they are released. In addition, writer can acquire read locks, but the reverse is not true.
- Lock down
Reentrant also allows you to downgrade from a write lock to a read lock by first acquiring the write lock, then acquiring the read lock, and finally releasing the write lock. However, upgrading from a read lock to a write lock is not possible.
- Interruption of lock acquisition
Both read and write locks support interruption during lock acquisition.
Condition
support
Write lock provides a Condition, for write lock, the implementation way and already. NewCondition () provides the Condition to realize the behavior to already had done the same. Of course, this Condition can only be used for write locks. Read locks do not support Condition.
- monitoring
This class supports methods to determine whether a lock is held or contorted. These methods are designed for monitoring system state, not for synchronous control.
Locks support a maximum of 65535 recursive write locks and 65535 read locks
The above is the explanation of the official Java Api document [1], which summarizes the content as follows:
- Supports unfair and fair modes. The default mode is unfair.
- The read lock can be reentered to acquire the read lock, and the write lock can be reentered to acquire the write lock. The write lock can acquire the read lock, but the read lock cannot acquire the write lock.
- A lock can be downgraded from a write lock to a read lock, but it cannot be upgraded from a read lock to a write lock.
The basic use
class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData(a) {
// read lock lock
rwl.readLock().lock();
if(! cacheValid) {// The read lock must be released before the write lock is acquired
rwl.readLock().unlock();
// write lock lock
rwl.writeLock().lock();
try {
// Recheck the status, as another thread may
// The write lock was obtained and the state changed before the operation was performed
if(! cacheValid) { data = ... cacheValid =true;
}
// Demote by acquiring the read lock before releasing the write lock
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // Unlock write, still hold read}}try {
use(data);
} finally{ rwl.readLock().unlock(); }}}Copy the code
The above is just a demo of the official documentation.
Question question
- What does state stand for in ReentrantReadWriteLock?
- What is the process of a thread acquiring a lock?
- How is reentrancy of read and write locks implemented?
- What happens if the current thread fails to acquire the lock and is blocked?
- How is lock degradation degraded?
Source code analysis
The code structure
public class ReentrantReadWriteLock implements ReadWriteLock.java.io.Serializable {
private static final long serialVersionUID = -6992448646407690164L;
/** Provides an inner class for reading locks */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Provides an inner class */ for write locking
private final ReentrantReadWriteLock.WriteLock writerLock;
/** Perform all synchronization mechanisms */
final Sync sync;
}
Copy the code
state
In the ReentrantLock source code, state represents the state of the lock. 0 indicates that no thread has held the lock, and greater than 1 indicates the number of threads that have held the lock and the number of reentrants. ReentrantReadWriteLock is a read-write lock, so you need to store read and write locks.
There is also a Sync inherited in ReentrantReadWriteLock AbstractQueuedSynchronizer, also FairSync, NonfairSync parent class. Some operations of state are defined internally.
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 6317671515068378041L;
/ / shift number
static final int SHARED_SHIFT = 16;
/ / unit
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
// Maximum number 1 << 16 -> 65536
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
// Use 1 << 16 -> 65536 to calculate exclusive numbers
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// Returns the number of share reservations
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// Returns the exclusive reservation number
static int exclusiveCount(int c) { returnc & EXCLUSIVE_MASK; }}Copy the code
In AQS, state is defined as an int. In ReentrantReadWriteLock, the high and low 16 bits of state are separated to indicate a read/write lock. 16 bits higher indicates a read lock, and 16 bits lower indicates a write lock. Use the sharedCount and exclusiveCount methods to obtain the current state of the read and write locks, respectively.
How to lock and release the lock from the Angle of read lock and write lock respectively?
ReadLock.lock
public static class ReadLock
implements Lock.java.io.Serializable {
/** * get read lock. * If the write lock is not held by another thread, the read lock is acquired and returned immediately. * If the write lock is held by another thread, the current thread is disabled for thread scheduling purposes and sleeps until the read lock is acquired. * /
public void lock(a) {
// call AQS to get shared resources
sync.acquireShared(1); }}Copy the code
Access to Shared resources, the use of the logic of AQS, including tryAcquireShared (arg) is in ReentrantReadWriteLock Sync. And as stipulated in AQS, tryAcquireShared is divided into three return values:
- Less than 0: failure.
- If the value is 0, resources are successfully obtained in shared mode, but subsequent nodes cannot be successfully obtained in shared mode.
- If the value is greater than 0, resources are successfully obtained in shared mode. Subsequent nodes may also succeed in obtaining resources in shared mode. In this case, subsequent waiting threads must check availability.
abstract static class Sync extends AbstractQueuedSynchronizer {
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
// Get the state value
int c = getState();
// If the exclusive count is not 0 and is not the current thread, there is a write lock
if(exclusiveCount(c) ! =0&& getExclusiveOwnerThread() ! = current)return -1;
// Get share count (read lock count)
int r = sharedCount(c);
// No need to block read locks && share count less than maximum && state Update successful
if(! readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {if (r == 0) {
// The current read lock count is 0
// firstReader is the first thread to acquire the read lock
// firstReaderHoldCount is the hold count for firstReader
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// Read lock reentrant
firstReaderHoldCount++;
} else {
// The current cache count
HoldCounter rh = cachedHoldCounter;
// The current thread did not count or did not create a counter
if (rh == null|| rh.tid ! = getThreadId(current))// Create count, based on ThreadLocal
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
// count up
rh.count++;
}
return 1;
}
// Get the shared lock method completely as if the tryAcquireShared method failed to get the lock due to CAS.
// The CAS fails or the queue policy fails.
returnfullTryAcquireShared(current); }}Copy the code
- Obtain the state. Use the exclusiveCount method to obtain the count of the write lock. The value is not 0 and is not the current thread, indicating that the write lock has been created. Return -1 failed.
- Use sharedCount to obtain the read lock count and determine whether blocking is required and whether the upper limit is exceeded. Then use CAS to update the read lock count.
- Set or update firstReader, firstReaderHoldCount, and cachedHoldCounter.
- Finally, a complete shared lock acquisition method is performed as a follow-up to the previous failed acquisition.
FirstReader: firstReader is the first thread to acquire the read lock; FirstReaderHoldCount: firstReaderHoldCount is the hold count for firstReader. That is, the number of reentries of the first thread that acquired the read lock. CachedHoldCounter: The number of reentrant times that the last thread that acquired the read lock acquired the read lock.
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
// Infinite loop
for (;;) {
int c = getState();
// Whether there is a write lock
if(exclusiveCount(c) ! =0) {
// There is a write lock, but it is not the current thread
if(getExclusiveOwnerThread() ! = current)return -1;
} else if (readerShouldBlock()) {
// Need to block
// There is no write lock to ensure that the read lock is not re-acquired
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
// The current thread's read lock count is in ThreadLocal
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null|| rh.tid ! = getThreadId(current)) { rh = readHolds.get();// When the count is complete, remove it
if (rh.count == 0) readHolds.remove(); }}// Fail directly with 0
if (rh.count == 0)
return -1; }}// An exception is thrown when the upper limit is reached
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// CAS sets the read lock
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null|| rh.tid ! = getThreadId(current)) rh = readHolds.get();else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1; }}}Copy the code
- First it’s going to keep going
- A write lock, but not the current thread, returns failure. However, if there is a write lock, the current thread will continue to execute.
- Set or update firstReader, firstReaderHoldCount, and cachedHoldCounter.
If there is a write lock (exclusive lock), the method returns -1 failure, and AQS ‘doAcquireShared method is called to loop the resource. The doAcquireShared method loops continuously, trying to acquire the read lock. Once the read lock is obtained, the current node immediately wakes up subsequent nodes, which then attempt to acquire the read lock and propagate in sequence.
ReadLock.unlock
public static class ReadLock
implements Lock.java.io.Serializable {
public void unlock(a) {
sync.releaseShared(1); }}Copy the code
Call AQS releaseShared to release the shared resource method.
TryReleaseShared has ReadLock implementation.
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
// The first thread is the current thread
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
// The first thread, not the current thread, updates the count in its ThreadLocal
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;
}
/ / loop
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
// Update state with CAS
if (compareAndSetState(c, nextc))
// But if both read and write locks are now released,
// It may allow waiting programs to continue.
return nextc == 0; }}Copy the code
- If it is the first thread, it updates the technique directly; if it is not, it updates the count stored in its ThreadLocal.
- Loop, using CAS to update the value of state.
- If the updated state value is 0, no thread holds the read or write lock.
- When state is 0, the doReleaseShared method of AQS is called. If there is a write lock on the queue, the lock will be acquired by the write lock.
WriteLock.lock
public static class WriteLock
implements Lock.java.io.Serializable {
/** * get write lock. * If no other thread holds a read or write lock, it returns directly and sets the write lock count to 1. * If the current thread holds a write lock, the write lock count is +1 and then returns. * If the lock is being held by another thread and the current thread is used for thread scheduling purposes, the current thread is disabled and sleeps until the read lock is acquired and the write lock count is set to 1. * /
public void lock(a) {
sync.acquire(1); }}Copy the code
The tryAcquire method is implemented by Write itself in a similar manner to ReentrantLock.
protected final boolean tryAcquire(int acquires) {
// If the read lock count is non-zero or the write lock count is non-zero, and the owner is another thread, it fails.
// If the count is saturated, it fails. This can only happen if count is not zero.
Otherwise, the thread is eligible to be locked if it is reentrant fetchable or if the queue policy allows it.
// If so, update the status and set the owner.
Thread current = Thread.currentThread();
int c = getState();
// Write lock count
int w = exclusiveCount(c);
/ / c! = 0 indicates that a thread has acquired the lock
if(c ! =0) {
// (Note: if c ! = 0 and w == 0 then shared count ! = 0)
// Return false if it is not itself
if (w == 0|| current ! = getExclusiveOwnerThread())return false;
// Determine if the upper limit is exceeded
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
/ / reentrant
setState(c + acquires);
return true;
}
// No blocking is required, or the CAS fails to update state
if(writerShouldBlock() || ! compareAndSetState(c, c + acquires))return false;
setExclusiveOwnerThread(current);
return true;
}
Copy the code
- Gets state. If state is not 0, determine whether the current thread is reentrant.
- If state is 0, the CAS of the current thread updates state and acquires the lock.
- Bind the current thread after the update is successful.
- If it fails, the AQS acquireQueued is continued, placing the current block on the AQS queue. AQS will loop over and over again, waiting for the last lock to be released and then trying to acquire it.
WriteLock.unlock
public static class WriteLock
implements Lock.java.io.Serializable {
// If the current thread is the holder of the lock, keep the count decrement.
// If the current count is kept at zero, the lock is unlocked.
/ / if the current thread is not the holder of the lock is IllegalMonitorStateException anomalies.
public void unlock(a) {
sync.release(1); }}Copy the code
Again, this code uses AQS logic, and the tryRelease part is implemented by WriteLock itself.
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;
}
Copy the code
- If the current thread is reentrant, the reentrant count is deducted.
- If the value is 0 after deduction, set the lock holding thread to NULL and update the state value. AQS wake up subsequent nodes to acquire locks.
conclusion
The problem
Q: What does state stand for in ReentrantReadWriteLock?
A: State indicates the lock status. State is 0, and no thread holds the lock. A high of 16 indicates the read lock state, and a low of 16 indicates the write lock state. Bitwise operations are used to obtain the actual value of the read/write lock.
Q: What is the process for threads to acquire locks?
A: You can refer to the source notes above and the flow chart below.
Q: How are read and write locks reentrant?
A: When locking, check whether it is the current thread. If it is the current thread, directly add the count. Note that read lock reentrant counts use ThreadLocal to cache the count in the thread, while write locks are directly accumulated with state (in the same way that state is accumulated 16 bits lower).
Q: What happens next if the current thread fails to acquire the lock and is blocked?
A: If the acquisition fails, it will be put into the AQS waiting queue. In the queue, it will keep circulating to monitor whether the previous node is head. If so, it will try again to obtain the lock.
Q: How is lock degradation degraded?
A: As shown in the fullTryAcquireShared code circled, the read lock can be acquired if the current thread holds the write lock. For example, thread A has acquired the write lock. When thread A finishes executing, it needs to acquire the current data. Assuming that lock degradation is not supported, thread A will release the write lock and request the read lock again. In between, it is possible for another blocking thread to acquire the write lock. This results in inconsistent data in thread A during A single execution.
summary
- ReentrantReadWriteLock read/WriteLock. Its internal implementation is ReadLock and WriteLock. Read locks, allowing sharing; Write locks are exclusive locks.
- Both read and write locks support reentrant. The number of reentrant counts for read locks is recorded in the ThreadLocal maintained by the thread, while the write locks are maintained on state (16 bits lower).
- Supports lock degradation from write lock to read lock, preventing dirty read.
- Both ReadLock and WriteLock are implemented via AQS. If the lock acquisition fails, it will be put into the AQS waiting queue, and subsequent attempts will be made to obtain the lock. A read lock is placed in the wait queue only when there is a write lock, whereas a write lock is placed in the wait queue whenever there is a non-current thread lock (either a write lock or a read lock). !
- Through source code analysis, it can be concluded that read and write lock is suitable for use in the scenario of read and write more.
The relevant data
[1] Java Api:docs.oracle.com/javase/8/do…