“This is the 16th day of my participation in the November Gwen Challenge. Check out the event details: The Last Gwen Challenge 2021


A ReentrantLock is an exclusive lock that can be accessed by only one thread at a time, but in most scenarios, the read service is available most of the time, while the write service occupies less time. However, read services do not have data contention problems. If one thread is reading while another thread is reading, performance will be degraded. So a read-write lock is provided.

Read/write locks maintain a pair of locks, one read and one write. By separating the read lock from the write lock, concurrency is greatly improved over the normal exclusive lock: multiple reader threads can be accessed at the same time, but all reader and writer threads are blocked when the writer thread accesses them.

Key features of read/write locks:

  1. Fairness: Supports both fairness and unfairness.
  2. Reentrant: Reentrant is supported. Read-write locks support a maximum of 65535 recursive write locks and 65535 recursive read locks.
  3. Lock degradation: A write lock can be degraded to a read lock in the order of obtaining a write lock and obtaining a read lock before releasing a write lock

ReentrantReadWriteLock The ReadWriteLock interface maintains a pair of related locks, one for read-only operations and one for write operations. As long as there is no writer, the read lock can be held by multiple reader threads simultaneously. The write lock is exclusive.

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}
Copy the code

ReadWriteLock defines two methods. ReadLock () returns the lock for the read operation, and writeLock() returns the lock for the write operation. ReentrantReadWriteLock is defined as follows:

/ * * inner class read lock * / private final ReentrantReadWriteLock. ReadLock readerLock; Write lock * / / * * inner class private final ReentrantReadWriteLock. WriteLock writerLock; final Sync sync; Public treadWritelock () {this(false); /** TreadWritelock () {this(false); } /** TreadWritelock public treadWritelock (Boolean fair) {sync = fair? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this); } / * * returns to write locks * / public ReentrantReadWriteLock. WriteLock WriteLock () {return writerLock; } / * * returns for read operations lock * / public ReentrantReadWriteLock. ReadLock ReadLock () {return readerLock; } stract static class Sync extends AbstractQueuedSynchronizer {/ * * * to omit the rest of the source code * /} public static class WriteLock Public static class implements Lock, java.io.Serializable{/** * omits other source code */} public static class implements Lock, Java.io.Serializable {/** * omit the rest of the source code */}Copy the code

ReentrantReadWriteLock is the same as ReentrantLock. The lock body of ReentrantReadWriteLock is Sync. The read and write locks of ReentrantReadWriteLock depend on Sync. So ReentrantReadWriteLock actually has only one lock. It only obtains the ReadLock and the writeLock in a different way. Its read and write locks are actually two classes: ReadLock and writeLock.

Synchronization status is represented by a state of type int in ReentrantLock, which represents the number of times the lock has been repeatedly acquired by a thread. But the read and write lock ReentrantReadWriteLock maintains two pairs of locks internally and requires a variable to maintain multiple states. Therefore, the read/write lock maintains this variable in a “bitwise cut use” manner, which splits it into two parts, with a high of 16 indicating a read and a low of 16 indicating a write. How does a read-write lock quickly determine the state of a read-write lock after splitting? By phi. If the current synchronization state is S, then the write state is equal to S & 0x0000FFFF (erase all 16 bits), and the read state is equal to S >>> 16(unsigned complement 0 moved 16 bits right). The code is as follows:

static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
Copy the code

Write lock

A write lock is an exclusive lock that supports reentrant.

Write lock acquisition

TryAcquire (int arg) is finally called to acquire the write lock, which is implemented in the inner class Sync:

protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread(); Int c = getState(); Int w = exclusiveCount(c); if (c ! = 0) { //c ! = 0 && w = = 0 indicates a read lock / / the current thread is not threads have access to write locks the if (w = = 0 | | current! = getExclusiveOwnerThread()) return false; If (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); setState(c + acquires); return true; } / / whether need to block the if (writerShouldBlock () | |! compareAndSetState(c, c + acquires)) return false; // Set the thread to acquire the lock to the current thread setExclusiveOwnerThread(current); return true; }Copy the code

This method, much like tryAcquire(int ARg) of ReentrantLock, adds a condition to determine reentry: whether a read lock exists. Because it is important to ensure that write lock operations are visible to the read lock, if a write lock is allowed to be acquired in the presence of a read lock, other threads that have acquired the read lock may not be aware of the current writer thread’s operations. Therefore, the write lock cannot be acquired by the current thread until the read lock is released completely. Once the write lock is acquired, all other read and write threads are blocked.

Write lock release

Unlock (); unlock(); unlock();

public void unlock() { sync.release(1); } public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h ! = null && h.waitStatus ! = 0) unparkSuccessor(h); return true; } return false; }Copy the code

TryRelease (int arg) ¶ tryRelease(int arg) is an attempt to release the lock. The tryRelease(int arg) method is defined in Sync as follows:

Protected final Boolean tryRelease(int releases) {// Release thread is not the lock holder 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

The write lock release process is similar to that of an exclusive lock ReentrantLock. Each release reduces the write state. When the write state is 0, the write lock is released completely.

Read lock

A read lock is a reentrant shared lock that can be held by multiple threads at the same time and is always or successfully acquired when no other writer thread accesses it.

Read lock acquisition

Read locks can be acquired by ReadLock’s lock() method:

public void lock() {
    sync.acquireShared(1);
}
Copy the code

Sync acquireShared(int arg) is defined in AQS:

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}
Copy the code

TryAcqurireShared (int arg) Attempts to obtain the read synchronization status. This method is mainly used to obtain the shared synchronization status. If the result is successful, >= 0 is returned; otherwise, < 0 is returned.

Protected final int tryAcquireShared(int unused) {// Thread current = thread.currentThread (); int c = getState(); If (exclusiveCount(c)! '//exclusiveCount(c)! = 0 && getExclusiveOwnerThread() ! = current) return -1; Int r = sharedCount(c); /* * readerShouldBlock(): whether to wait to read the lock(fair lock principle) * r < MAX_COUNT: hold less than the maximum number of threads (65535) * compareAndSetState(c, c + SHARED_UNIT) : */ if (! readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, */ 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; } return fullTryAcquireShared(current); }Copy the code

The process of acquiring read locks is slightly more complicated than that of acquiring exclusive locks. The whole process is as follows:

  1. Because there is lock degradation, failure is returned if there is a write lock and the owner of the lock is not the current thread, otherwise continue
  2. If the number of threads holding the read lock is less than the maximum value (65535) and the lock status is set successfully, execute the following code (explained below for HoldCounter) and return 1. If not, execute fullTryAcquireShared().
final int fullTryAcquireShared(Thread current) { HoldCounter rh = null; for (;;) { int c = getState(); If (exclusiveCount(c)! = 0) { if (getExclusiveOwnerThread() ! = current) return -1; Else if (readerShouldBlock()) {if (firstReader == current) {} else {if (rh == rh null) { rh = cachedHoldCounter; if (rh == null || rh.tid ! = getThreadId(current)) { rh = readHolds.get(); if (rh.count == 0) readHolds.remove(); } } if (rh.count == 0) return -1; If (sharedCount(c) == MAX_COUNT) throw new Error("Maximum lock count exceeded"); If (compareAndSetState(c, c + SHARED_UNIT)) {compareAndSetState(c, c + SHARED_UNIT)) { FirstReaderHoldCount (sharedCount(c) == 0) {firstReader = current; firstReaderHoldCount = 1; FirstReaderHoldCount +1 else if (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 ++; rh.count++; cachedHoldCounter = rh; // cache for release } return 1; }}}Copy the code

FullTryAcquireShared (Thread Current) is processed based on “whether to block wait”, “whether the share count for reading locks has exceeded the limit”, etc. If no blocking wait is required and the lock share count does not exceed the limit, an attempt is made to acquire the lock through CAS and 1 is returned

Read lock release

Like write locks, read locks provide unlock() to release read locks:

public void unlock() {
    sync.releaseShared(1);
}
Copy the code

The unlcok() method internally uses Sync’s releaseShared(int arg) method, which is defined in AQS:

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
Copy the code

TryReleaseShared (int arg) is called to attempt to release the read lock. This method is defined in the Sync inner class that reads and writes the lock:

protected final boolean tryReleaseShared(int unused) { Thread current = Thread.currentThread(); If (firstReader == current) {if (firstReader == current) { Otherwise firstReaderHoldCount - 1 if (firstReaderHoldCount == 1) firstReader = null; else firstReaderHoldCount--; Else {HoldCounter RH = cachedHoldCounter;} // Get rh object and update "current thread get lock information" else {HoldCounter RH = cachedHoldCounter; if (rh == null || rh.tid ! = getThreadId(current)) rh = readHolds.get(); int count = rh.count; if (count <= 1) { readHolds.remove(); if (count <= 0) throw unmatchedUnlockException(); } --rh.count; } //CAS updates the synchronization status for (;;) { int c = getState(); int nextc = c - SHARED_UNIT; if (compareAndSetState(c, nextc)) return nextc == 0; }}Copy the code

HoldCounter

Read lock There is always a variable RH (HoldCounter) that plays an important role in the process of acquiring and releasing a lock.

We understand that the internal mechanism of a read lock is actually a shared lock. To better understand the HoldCounter, we temporarily consider it not the probability of a lock, but equivalent to a counter. An operation on a shared lock is equivalent to an operation on that counter. If the shared lock is acquired, the counter + 1 is released, and the counter -1 is released. The thread can release and re-enter the shared lock only after acquiring it. So a HoldCounter is the number of shared locks held by the current thread. This number must be tied to the thread, otherwise handling another thread lock will raise an exception. Let’s look at the HoldCounter definition first:

static final class HoldCounter {
    int count = 0;
    final long tid = getThreadId(Thread.currentThread());
}
Copy the code

The HoldCounter definition is very simple, consisting of a counter count and a thread ID TID. This means that a HoldCounter needs to bind to a thread. We know that to bind an object to a thread, it is not enough to have a TID. Besides, we can see from the above code that a HoldCounter only records the TID and does not bind to a thread. So how do you do that? The answer is ThreadLocal, defined as follows:

static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> { public HoldCounter initialValue() { return new HoldCounter(); }}Copy the code

You can bind the thread with the code HoldCounter above. Therefore, a HoldCounter should be a counter on the binding thread, and a ThradLocalHoldCounter is a ThreadLocal for the thread binding. From the above we can see that ThreadLocal binds the HoldCounter to the current thread, and the HoldCounter also holds the thread Id, This allows you to know when the lock is released whether the last reader thread (cachedHoldCounter) cached in ReadWriteLock is the current thread. This has the advantage of reducing the number of threadLocal.get () operations, as this is also a time-consuming operation. Note that the reason HoldCounter binds thread ids instead of thread objects is so that HoldCounter and ThreadLocal are not bound to each other and the GC is unable to release them (although the GC can find such references intelligently and reclaim them at a cost). So this is really just to help the GC reclaim objects quickly.

Now that we understand what a HoldCounter does, we’re looking at a code snippet that acquires a read lock:

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 }Copy the code

This code involves several variables: firstReader, firstReaderHoldCount, and cachedHoldCounter. Let’s be clear about these variables:

private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
private transient HoldCounter cachedHoldCounter;
Copy the code

FirstReader is the first thread to acquire the read lock, firstReaderHoldCount is the first reentrantnumber to acquire the read lock, and cachedHoldCounter is the cache of the HoldCounter.

Now that all the above variables are clear and the HoldCounter is clear, let’s comment the above code as follows:

FirstReaderHoldCount reentrant + 1 else if (firstReader == current) {firstReaderHoldCount++; } else {// Non-firstReader count if (rh == null) rh = cachedHoldCounter; //rh == null or rh.tid! = current. GetId (), it is necessary to obtain rh if (rh = = null | | rh. Dar! = getThreadId(current)) rh = readHolds.get(); Else if (rh.count == 0) readHolds. Set (RH); // count+ 1 rh.count++; cachedHoldCounter = rh; // cache for release }Copy the code

Here’s why we introduced firstRead, firstRead holdcount. For efficiency reasons, firstReaders will not be placed in readHolds, and looking for readHolds will be avoided if only one lock is read.

Lock down

Lock degradation means that a write lock can be degraded to a read lock, but it needs to follow the sequence of obtaining the write lock first, obtaining the read lock and releasing the write lock. Note that if the current line first acquires the write lock, then releases the write lock, and then acquires the read lock, this process is not called lock degradation. Lock degradation must follow that order.

In the tryAcquireShared(int unused) method, a code is used to read the degraded lock:

int c = getState(); If (exclusiveCount(c)! '//exclusiveCount(c)! = 0 && getExclusiveOwnerThread() ! = current) return -1; Int r = sharedCount(c);Copy the code

Is read lock acquisition release necessary in lock degradation? Definitely necessary. For example, if thread A does not acquire the read lock but releases the write lock, and another thread B acquires the write lock, the changes made by thread B will not be visible to thread A. If the read lock is obtained, thread B will block if the read lock is not released during the process of obtaining the write lock. Thread B will obtain the write lock successfully only after the current thread A releases the read lock.

Recommended reading

Because there are many areas covered in the AQS section, it is recommended to read the following:

  1. AQS at —–J.U.C: An introduction to AQS
  2. AQS of —–J.U.C: CLH synchronization queues
  3. AQS of —–J.U.C: Obtaining and releasing synchronization state
  4. AQS of —–J.U.C: Blocking and waking up threads

The resources

  1. Doug Lea: Java Concurrent Programming in Action
  2. Fang Tengfei: The Art of Java Concurrent Programming
  3. “J.U.C” : ReentrantReadWriteLock
  4. Java Multithreading (10) in-depth analysis of ReenTrantreadWrite Ock