preface

In my last blog post, I talked about ReentrantLock, but ReentratLock is an exclusive lock, which can cause poor performance when you write less and read more. The JUC package also provides a ReentrantReadWriteLock lock that can be accessed by multiple threads simultaneously using read/write separation. This article explains and records the initialization, acquisition, and release of ReentrantReadWriteLock from a source code perspective.

Lock initialization

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable { private static final long serialVersionUID = -6992448646407690164L; / / read lock object private final ReentrantReadWriteLock. ReadLock readerLock; / / write lock object private final ReentrantReadWriteLock. WriteLock writerLock; final Sync sync; public ReentrantReadWriteLock() { this(false); } public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this); }Copy the code

The read/write lock has two attributes, readerLock and writerLock, respectively. As with ReentrantLock, there is also an object of type Sync, Sync, that does the locking. When initialized, the default is implemented in an unfair way (NonfairSync and FairSync are subclasses of Sync, which are similar to ReentrantLock)

Let’s look at the readerLock and writerLock properties:

Public static class implements Lock, java.io.Serializable { private static final long serialVersionUID = -5992448646407690164L; private final Sync sync; . protected ReadLock(ReentrantReadWriteLock lock) { sync = lock.sync; } public static class WriteLock implements Lock, java.io.Serializable { private static final long serialVersionUID = -4992448646407690164L; private final Sync sync; . protected WriteLock(ReentrantReadWriteLock lock) { sync = lock.sync; }Copy the code

As you can see, they all have a Sync property of type Sync. When they are initialized, they assign Sync of the ReentrantReadWriteLock object to their own Sync property (because they are called by readerLock = new ReadLock(this); WriterLock = new WriteLock(this);) .

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


public void lockInterruptibly() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
Copy the code

Here are some of the methods in ReadLock, and as you can see, some of the functionality is also implemented through sync. WriteLock similarly.

Read lock, write lock status acquisition and update

The read/write lock has two states: read/write lock, but the read/write lock does not define any new variables, but uses the state variable in AQS. The high 16 bits of the variable denote the read state, and the low 16 bits denote the write state. Here is the code for read and write state acquisition:

Static final int SHARED_SHIFT = 16; 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; /** Returns the number of shared holds represented in count. */ static int sharedCount(int c) { return c >>> SHARED_SHIFT; } /** Returns the number of exclusive holds represented in count. */ static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }Copy the code

Return c >>> SHARED_SHIFT; SHARED_SHIFT is 16, that is, move c 16 bits to the right, a 32-bit variable 16 bits to the right, that is, read the highest 16 bits of the variable.

Int nextc = c-shared_unit; if (compareAndSetState(c, nextc)) ... CompareAndSetState (c, c + SHARED_UNIT))Copy the code

The above is part of the code for acquiring and releasing the read lock. In this part, update the read lock status mainly by adding and subtracting the SHARED_UNIT variable. The value of this variable is (1 << SHARED_SHIFT), which is 10000000000000000 (binary, 1 followed by 16 zeros). You can see that each increment or subtraction of SHARED_UNIT has no effect on the lower 16 bits.

Now let’s look at write locks. Return c & EXCLUSIVE_MASK; , this EXCLUSIVE_MASK value is (1 << SHARED_SHIFT) -1; 1111111111111111 (binary, 16 (1), high 16 full complement of 0, and a 32-bit variables for & operation, the high number of 16 nature are all zero, low 16 remains unchanged, the original is 0 or 0, turned out to be 1 or 1.

Int nexTC = getState() -releases; setState(nextc); . SetState (c + acquires);Copy the code

As you can see, the state of the write lock is directly added and subtracted, after all, it operates on the lower 16 bits. However, this can be risky, because 16 bits are a finite number of numbers, so this code is often used:

if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); . if (sharedCount(c) == MAX_COUNT) throw new Error("Maximum lock count exceeded");Copy the code

The MAX_COUNT value is (1 << SHARED_SHIFT) -1, which is 1111111111111111 (binary, 16 ones). This is the limit of 16-bit representation. So if it exceeds, an exception is thrown.

I have to say, this is a very clever operation. If I were to implement it myself, I would definitely redefine two properties, one for read and one for write. They’re still using the state variable, but it’s a nice trick to have one variable representing two states, and still use a function of state. And the MAX_COUNT decimal number is 65536, so no program lock can count that many.

Write lock

Write lock acquisition

Note: lockInterruptibly() and public Boolean tryLock(long Timeout, TimeUnit Unit) are not considered for write lock acquisition and release. Those other cases will have a separate summary when they are free, the same as reading locks

public void lock() { sync.acquireShared(1); Public final void acquireShared(int arg) {if (tryAcquireShared(arg) < 0) acquire(null, arg, true, false) false, 0L); } protected final Boolean tryAcquire(int acquires) {Thread current = thread.currentThread (); int c = getState(); int w = exclusiveCount(c); if (c ! = 0) { // (Note: if c ! = 0 and w == 0 then shared count ! = 0) if (w == 0 || current ! = getExclusiveOwnerThread()) return false; if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); // Reentrant acquire setState(c + acquires); return true; } if (writerShouldBlock() || ! compareAndSetState(c, c + acquires)) return false; setExclusiveOwnerThread(current); return true; }Copy the code

The above invocation relationship is simple, similar to ReentrantLock, so I won’t cover it. Focus on the tryAcquire() function. The first is the if (w = = 0 | | current! = getExclusiveOwnerThread()), which returns false. Notice, this is in if (c! C! = 0). =0,w is 0, indicating that there is a thread holding the read lock, so it cannot be acquired; Or even though W! = 0, but the thread that holds the write lock is not the same thread. If (w + exclusiveCount(acquires) > MAX_COUNT) If none of these branches are available, the current thread is holding the write lock and will not overflow. Set state to ‘ExclusiveOwnerThread’. And setState() is called, because in this case, the lock is held by the local thread, there is no multi-thread contention, and no CAS operation is required.

If c is not 0, the lock is not held by any thread. Call writerShouldBlock() to see if this thread needs to block, and then CAS to set the lock state. If it succeeds, setExclusiveOwnerThread(current) is called. To set the lock holder. The CAS operation is used because the lock is not held by any thread and therefore needs to be contended.

Look at writerShouldBlock() :

// Final Boolean writerShouldBlock() {return false; // Writers can always barge} // Final Boolean writerShouldBlock() {return hasqueued24 (); // Writers can always barge} // Final Boolean writerShouldBlock() {return hasqueued24 (); }Copy the code

An unfair lock returns false directly, and the fair lock calls hasqueuedBoomon () in AQS; Checks whether the current thread has a precursor node. This is a short-circuit operation of a logical expression. If is a fair lock, the if (writerShouldBlock () | |! CompareAndSetState (c, c + acquires)) return false; If it is a fair lock, call hasqueued24 (); If returns true, because it is | | operation, after a condition without judging, the logical expression directly returns true, otherwise, will go under one condition. If the | | to | wouldn’t work. This function if LET me write, I must be if if if, the author of the source code clever use of short-circuit operation, streamlined code, the level is really high ah.

public boolean tryLock() { return sync.tryWriteLock(); }... final boolean tryWriteLock() { Thread current = Thread.currentThread(); int c = getState(); if (c ! = 0) { int w = exclusiveCount(c); if (w == 0 || current ! = getExclusiveOwnerThread()) return false; if (w == MAX_COUNT) throw new Error("Maximum lock count exceeded"); } if (! compareAndSetState(c, c + 1)) return false; setExclusiveOwnerThread(current); return true; }Copy the code

And tryLock(). Similar to normal lock(). The difference is:

  • When c=0, it’s not calledwriterShouldBlock()The CAS function directly sets the state of the lock
  • The call returns true or false and does not block the queue

Write lock release

public void unlock() { sync.release(1); Public final Boolean release(int arg) {if (tryRelease(arg)) {signalNext(head); return true; } return false; } // 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

This is similar to ReentrantLock. In tryRelease, you first check to see if the current thread actually holds the lock, and if it doesn’t, you release nothing. Free = exclusiveCount(nexTC) == 0; SignalNext (head) in AQS to indicate whether the lock is released clean. if so, signalNext(head) in AQS. Call up the next thread.

In general, the write lock part is similar to ReentrantLock, nothing too difficult.

Read lock

Read lock acquisition

Try to get

public void lock() { sync.acquireShared(1); Public final void acquireShared(int arg) {if (tryAcquireShared(arg) < 0) acquire(null, arg, true, false) false, 0L); } protected final int tryAcquireShared(int unused) {Thread current = thread.currentThread (); int c = getState(); if (exclusiveCount(c) ! = 0 && getExclusiveOwnerThread() ! = current) return -1; int r = sharedCount(c); if (! readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, If (r == 0) {firstReader = current; if (r == 0) {firstReader = current; firstReaderHoldCount = 1; Else if (firstReader == current) {firstReaderHoldCount++; // This thread is not the first thread to acquire the read lock} else {HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid ! = LockSupport.getThreadId(current)) cachedHoldCounter = rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); rh.count++; } return 1; } return fullTryAcquireShared(current); }Copy the code

Call relationships are simple, needless to say. Focus on the tryAcquireShared function. If (exclusiveCount(c)! = 0 && getExclusiveOwnerThread() ! Return -1 if another thread holds the write lock. It’s ok to hold the write lock, you can go down.

This is followed by another short circuit: if (! ReaderShouldBlock () &&r < MAX_COUNT &&compareAndSetState (c, c + SHARED_UNIT)), when! ReaderShouldBlock () and r < MAX_COUNT are both true, and the third judgment, CAS, is used to set the lock. When all three conditions are true, it means that the lock has been set successfully, and the seemingly unreadable code in the code block will be executed. Of course, it is difficult to satisfy all three conditions, so if the logical expression is not valid, fullTryAcquireShared(current) will be called to obtain further. As you can see, tryAcquireShared is just making a try.

Now look at the code where the logical expression is true.

private transient Thread firstReader;
private transient int firstReaderHoldCount;
static final class HoldCounter {
    int count;          // initially 0
    // Use id, not reference, to avoid garbage retention
    final long tid = LockSupport.getThreadId(Thread.currentThread());
}

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

private transient ThreadLocalHoldCounter readHolds;
private transient HoldCounter cachedHoldCounter;
Copy the code

This is the definition of the variable that appears in that code. A fistReader is used to record the first thread to acquire a read lock, and a fitstReaderHoldCount is used to record the number of threads held by that thread (ReentrantReadWriteLock is also reentrant). ReadHolds are objects of ThreadLocalHoldCounter, which is a subclass of ThreadLocal. This ThreadLocal holds the object of the HoldCounter class, which contains the number of holds and the id of the holding thread. ((φ(◎ロ)) Phi))).

This thread is the first to set the lock count from 0 to 1, so it sets firstReader and firstReaderHoldCount. Otherwise, if the first thread that holds the lock is the same thread, it makes sense to just ++firstReaderCount. The code in these two places is also not synchronized, because r is the previous read lock value, CAS has successfully set the read lock state when entering the branch r=0, so other threads will definitely not be able to reach the branch r=0. Else if (firstReader == current) branch must be triggered only if this thread is firstReader.

If neither condition is met, the thread is the second and subsequent thread to acquire the read lock. At this point, the thread maintains its own read lock count. The code in this branch operates on the read lock count for this thread. So HoldCounter RH = cachedHoldCounter; Some books say that cachedHoldCounter is the last thread to acquire a read lock. I don’t think so. After all, the variable is not volatile, visibility is not guaranteed, and what you read is not necessarily the last one fetched. First the if (rh = = null | | rh. Dar! = locksupport. getThreadId(current)) if not, the rh obtained from cachedHoldCounter is the rh of this thread; If rh is not of this thread, pass cachedHoldCounter = rh = readHolds. Get (); When set, RH also becomes the HoldCounter variable for this thread.

Else if (Rh. count == 0) branch. If you can access this branch, it means that the HoldCounter held by cachedHoldCounter is actually the thread’s HoldCounter, but the corresponding count is 0. So why does this happen? Because the release of the read lock does not clear the code for cachedHoldCounter. So the thread corresponding to cachedHoldCounter’s previous read lock was released once, and the thread is again acquiring the read lock, so it assigns its own HoldCounter variable to it.

Anyway, when we go to rh.count++; When this statement is made, rh must correspond to the HoldCount object of the thread. Increment its count by one.

This code is really uncomfortable when I look at it, it took me a long time to understand. There is no firstReader firstReaderHoldCount, cachedHoldCounter nor not. Since a HoldCounter is a ThreadLocal, each thread can read it from its own thread. But that might be a little bit inefficient, so I’ve set up a little bit of a cache of variables, and if they hit, they don’t need to be read by their own thread. You can tell by its name: cachedHoldCounter.

Fully get

final int fullTryAcquireShared(Thread current) { HoldCounter rh = null; for (;;) { int c = getState(); If (exclusiveCount(c)! = 0) { if (getExclusiveOwnerThread() ! = current) return -1; // else we hold the exclusive lock; Blocking here // Would cause deadlock. // Check whether this thread has a read lock} else if (readerShouldBlock()) {// Make sure we're not acquiring read lock reentrantly if (firstReader == current) { // assert firstReaderHoldCount > 0; } else {if (rh == null) {// Try to get thread HolderCounter rh = cachedHoldCounter; / / if cachedHoldCounter didn't get to, get if from the ThreadLocal (rh = = null | | rh. Dar! = LockSupport.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)) {if (sharedCount(c) == 0) {firstReader = current; firstReaderHoldCount = 1; } else if (firstReader == current) { firstReaderHoldCount++; } else { if (rh == null) rh = cachedHoldCounter; if (rh == null || rh.tid ! = LockSupport.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

The code first checks whether the write lock for this lock is held. As can be seen from the code, when the write lock is held by the thread, it is possible to acquire the read lock; If another thread holds the write lock, -1 is returned.

Next go to the else if (readerShouldBlock()) {branch. Entering this branch means that the write lock is not held by another thread, but that thread needs to block to acquire the read lock.

/ / not fair lock to realize the final Boolean readerShouldBlock () {return apparentlyFirstQueuedIsExclusive (); } // Implement final Boolean readerShouldBlock() {return hasqueued24 (); }Copy the code

These are all functions defined in the AQS class, so I won’t go into them here. So why is there a bunch of code in this case? Why don’t you just return -1?

if (rh.count == 0)
    return -1;
Copy the code

The key code for this is actually right here. This part is to determine whether the thread already holds the read lock. According to the source code, the Java developers believe that if the thread reentrant the read lock, even if readerShouldBlock() is true, it can go to the next part to obtain the read lock. If (firstReader == current) is true, then it must be reentrant and can proceed to the next step; Otherwise, it will attempt to hit the cache again with cachedHoldCounter. If it doesn’t hit, it will read the HoldCounter object from its own thread, as explained earlier.

If none of this code returns, then the thread is allowed to acquire the read lock, so CAS is performed to set the read lock status. If (compareAndSetState(c, c + SHARED_UNIT)) if (compareAndSetState(c, C + SHARED_UNIT)) if (compareAndSetState(c, C + SHARED_UNIT))) if (compareAndSetState(c, C + SHARED_UNIT))) if (compareAndSetState(c, C + SHARED_UNIT))) Otherwise, notice that the entire code is wrapped in a for (;;) In, the thread keeps trying the CAS operation.

A tryReadLock() loop is used to obtain the read lock. The code is basically the same.

public boolean tryLock() { return sync.tryReadLock(); } final Boolean tryReadLock() {Thread current = thread.currentThread (); for (;;) { int c = getState(); if (exclusiveCount(c) ! = 0 && getExclusiveOwnerThread() ! = current) return false; int r = sharedCount(c); if (r == MAX_COUNT) throw new Error("Maximum lock count exceeded"); if (compareAndSetState(c, c + SHARED_UNIT)) { if (r == 0) { firstReader = current; firstReaderHoldCount = 1; } else if (firstReader == current) { firstReaderHoldCount++; } else { HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid ! = LockSupport.getThreadId(current)) cachedHoldCounter = rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); rh.count++; } return true; }}}Copy the code

Read lock release

public void unlock() { sync.releaseShared(1); } public Final Boolean releaseShared(int arg) {if (tryReleaseShared(arg)) {signalNext(head); return true; } return false; } protected final Boolean tryReleaseShared(int unused) {thread.currentThread (); if (firstReader == current) { // assert firstReaderHoldCount > 0; if (firstReaderHoldCount == 1) firstReader = null; else firstReaderHoldCount--; } else { HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid ! = LockSupport.getThreadId(current)) rh = readHolds.get(); int count = rh.count; if (count <= 1) { readHolds.remove(); if (count <= 0) throw unmatchedUnlockException(); } --rh.count; } for (;;) { int c = getState(); int nextc = c - SHARED_UNIT; if (compareAndSetState(c, nextc)) // Releasing the read lock has no effect on readers, // but it may allow waiting writers to proceed if // both read and write locks are now free. return nextc == 0; }}Copy the code

The first branch is the if (firstReader == current) branch, which means that this thread is the first one to acquire the read lock.

If (firstReader == current) else branch = 1 If (count <= 1), the lock count will be 0 and needs to be released, so readHolds. Remove (); Action to clear the thread’s HoldCounter object. If if (count <= 0) is found, there is nothing to free at all, and an exception is thrown.

After that, the loop CAS sets the read lock state.

Other methods

So far, the more difficult code for reading and writing locks has been explained. There are also some very simple methods such as the following:

public final boolean isFair() { return sync instanceof FairSync; }... protected Thread getOwner() { return sync.getOwner(); } // other methodsCopy the code

It’s all so simple, there’s nothing to say.

conclusion

In this article, ReentrantReadWriteLock is studied and recorded at the source code level. The main difficulty of ReentrantReadWriteLock is obtaining and releasing read locks. This is my personal experience in the learning process. If there is any incomplete understanding, please feel free to communicate in the comments section.