1. Read/write lock introduction

In reality, there is a scenario where read and write operations are performed on shared resources, and the write operations are not as frequent as the read operations. There is no problem with multiple threads reading a resource simultaneously when there is no write operation, so multiple threads should be allowed to read a shared resource simultaneously. However, if one thread wants to write to a shared resource, no other thread should be allowed to read or write to the resource.

For this scenario, JAVA provides ReentrantReadWriteLock, a read and write lock. ReentrantReadWriteLock is a shared lock related to read operations. One is a write-related lock, called an exclusive lock, described as follows:

Prerequisites for a thread to enter a read lock:

There are no write locks for other threads,

There is no write request or there is a write request, but the calling thread is the same as the thread holding the lock.

A thread can enter a write lock if:

There are no read locks for other threads

There are no write locks for other threads

Read/write locks have three important features:

(1) Fair selection: support unfair (default) and fair lock acquisition methods, throughput is still unfair is better than fair.

(2) Re-entry: Both read and write locks support thread re-entry.

(3) Lock degradation: a write lock can be degraded to a read lock by following the sequence of obtaining a write lock, obtaining a read lock, and then releasing the write lock.

Second, source code interpretation

Let’s take a look at the structure of the ReentrantReadWriteLock class:

public class ReentrantReadWriteLock implements ReadWriteLock, Java. IO. Serializable {/ * * * read lock/private final ReentrantReadWriteLock. ReadLock readerLock; Write lock / * * * / 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; } abstract static class Sync extends AbstractQueuedSynchronizer {} static final class NonfairSync extends Sync {} static  final class FairSync extends Sync {} public static class ReadLock implements Lock, java.io.Serializable {} public static class WriteLock implements Lock, java.io.Serializable {} }Copy the code

1. Class inheritance

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {}
Copy the code

Note: you can see, ReentrantReadWriteLock implements ReadWriteLock interface, ReadWriteLock interface defines the access to read and write locks lock specification, specific implementation class to be realized; The Serializable interface can be serialized. ReentrantReadWriteLock implements its own serialization logic in the source code.

Class inner class

ReentrantReadWriteLock has five inner classes that are also related to each other. The inner class relationship is shown below.

Note: As shown in the figure above, Sync inherits from AQS, NonfairSync inherits from Sync class, and FairSync inherits from Sync class (Boolean values passed in the constructor determine which Sync instance to construct). ReadLock implements the Lock interface, and WriteLock implements the Lock interface.

Sync:

(1) Class inheritance relationship

abstract static class Sync extends AbstractQueuedSynchronizer {}
Copy the code

The Sync abstract class inherits from the AQS abstract class. Sync provides support for ReentrantReadWriteLock.

(2) The inner class of the class

There are two internal classes in Sync class, respectively HoldCounter and ThreadLocalHoldCounter. HoldCounter is mainly used with read lock. HoldCounter source code is as follows.

Static final class HoldCounter {// count static final class HoldCounter = 0; // Use id, not reference, Final long TID = getThreadId(thread.currentThread ()); }Copy the code

Note: HoldCounter has two main attributes, count and tid, where count represents the number of times a reader thread reenters, tid represents the value of the tid field of the thread, which can be used to uniquely identify a thread. The source code for ThreadLocalHoldCounter is as follows

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

Note: ThreadLocalHoldCounter overrides the initialValue method of ThreadLocal, which can associate a thread with an object. In the absence of a set, all you get is the HolderCounter object generated in the initialValue method.

(3) Class attributes

The abstract static class Sync extends AbstractQueuedSynchronizer {/ / version serial number private static final long serialVersionUID = 6317671515068378041L; 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; Private TRANSIENT ThreadLocalHoldCounter readHolds; Private TRANSIENT HoldCounter cachedHoldCounter; Private TRANSIENT Thread firstReader = null; Private TRANSIENT int firstReaderHoldCount; private TRANSIENT int firstReaderHoldCount; }Copy the code

Note: This property contains the maximum number of lock read and lock write threads. Local thread counters, etc.

(4) Class constructor

// 构造函数
Sync() {
    // 本地线程计数器
    readHolds = new ThreadLocalHoldCounter();
    // 设置AQS的状态
    setState(getState()); // ensures visibility of readHolds
}
Copy the code

Note: The local thread counter and AQS state are set in the Sync constructor.

3. Design of read and write state

The synchronization status in the reentrant lock implementation is the number of times it is repeatedly acquired by the same thread, i.e. maintained by an integer variable, but the previous representation only indicates whether the lock is locked, not whether it is read or write. Read/write locks need to maintain the state of multiple readers and one writer on a synchronous state (an integer variable).

Read/write locks are implemented in a synchronous state by “bitwise cutting” on an integer variable: the variable is cut in two, with 16 bits higher indicating read and 16 bits lower indicating write.

Assuming that the current synchronization status is S, the operations of get and set are as follows:

(1) Obtain write status:

S&0x0000FFFF: Erase all the high 16 bits

(2) Obtain read state:

S>>>16: unsigned complement 0, move 16 bits right

(3) Write state + 1:

S+1

(4) Read state + 1:

S+ (1<<16) is S+ 0x00010000

In the code level judgment, if S is not equal to 0, when the write state (S&0x0000FFFF) and the read state (S>>>16) is greater than 0, it indicates that the read lock of the read/write lock has been acquired.

4. Write lock acquisition and release

Look at the Lock and unlock methods in the WriteLock class:

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

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

As you can see, the exclusive synchronization state of the call is acquired and released, so the real implementation is tryAcquire and tryRelease of Sync.

To obtain a write lock, see tryAcquire:

1 protected final Boolean tryAcquire(int acquires) {2 Thread current = thread.currentThread (); Int c = getState(); Int w = exclusiveCount(c); 7 int w = exclusiveCount(c); 8 9 // Current synchronization status state! If = 0, another thread has acquired the read or write lock. If the write lock status is 0, the read lock is occupied. 12 / / if the write lock state of 0 and not written by the current thread lock is not held returns false if 13 (w = = 0 | | current! = getExclusiveOwnerThread()) 14 return false; 15 16 // Check whether the same thread obtains the write lock for the maximum number of times (65535), 17 if (w + exclusiveCount(acquires) > MAX_COUNT) 18 throw new Error("Maximum lock count exceeded"); 19 // Update status 20 // The current thread already holds the write lock. It is reentrant, so you only need to change the number of locks. 21 setState(c + acquires); 22 return true; 23} 24 to 25 / / here shows the c = 0, read and write locks lock has not been for 26 / / writerShouldBlock indicates whether or not the block 27 if (writerShouldBlock () | | 28! compareAndSetState(c, c + acquires)) 29 return false; 30 31 // Set the lock to the current thread. 32 setExclusiveOwnerThread(current); 33 return true; 34}Copy the code

ExclusiveCount: number of threads holding write lock

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

Note: The state and (2^ 16-1) are calculated directly, which is equivalent to the state module on 2^16. The number of write locks is represented by the lower 16 digits of state.

As can be seen from the source code, the steps to obtain the write lock are as follows:

(1) Obtain C and W first. C indicates the current lock status. W is the number of writer threads. Then check whether the synchronization state is 0. If the state! If =0, it indicates that another thread has acquired the read or write lock, and execute (2). Otherwise, perform (5).

(2) If the lock state is not zero (c! W = 0, w = 0, w = 0, w = 0, w = 0, w = 0 Or the lock state is not zero and the write lock state is not zero, but the thread that obtains the write lock is not the current thread, so the current thread cannot acquire the write lock.

(3) Judge whether the current thread has acquired the write lock for more than the maximum number of times, if so, throw the exception, otherwise update the synchronization status (at this time the current thread has acquired the write lock, update is thread safe), return true.

(4) if the state is 0, the read or write locks lock is not available, determine whether to block (fair and not fair way to achieve different), under the fair strategy always will not be blocked, under a fair strategy will judge, judge whether there is a waiting time synchronization the queue longer threads, if any, you may need to be blocked, otherwise, no obstruction), If no blocking is required, the CAS updates the synchronization status. On success, the CAS returns true, on failure, the lock was seized by another thread, and false. Also return false if blocking is required.

(5) After successfully acquiring the write lock, set the current thread as the thread owning the write lock, return true.

The method flow chart is as follows:

To release the write lock, tryRelease:

1 Protected final Boolean tryRelease(int Releases) {2 // If the lock holder is not the current thread, throw an exception 3 if (! isHeldExclusively()) 4 throw new IllegalMonitorStateException(); Int nexTC = getState() -releases; 7 Boolean Free = exclusiveCount(nexTC) == 0; 8 Boolean Free = exclusiveCount(nexTC) == 0; 9 if (free) 10 // Set the lock holder to NULL if the number of new threads writing locks is 0. 11 setExclusiveOwnerThread(null); 12 // Set the number of new threads to write the lock 13 // Update the exclusive reentrant 14 setState(nexTC); 15 return free; 16}Copy the code

The process of releasing a write lock is relatively simple: first check to see if the current thread is the holder of the write lock, and if not throw an exception. Then check whether the number of threads writing the lock after release is 0. If it is 0, it indicates that the lock is free. Releasing the lock resource sets the thread holding the lock to NULL, otherwise releasing the lock is only a re-entry, and cannot empty the thread writing the lock.

Note: This method is used to release write lock resources. First, it will determine whether the thread is an exclusive thread. If it is not, an exception will be thrown. The flow chart of the method is as follows.

5. Obtain and release the read lock

Similar to write locks, the actual implementations of lock and unlock for read locks correspond to Sync’s tryAcquireShared and tryReleaseShared methods.

To get a read lock, see the tryAcquireShared method

1 protected final int tryAcquireShared(int unused) {3 Thread current = thread.currentThread (); Int c = getState(); 6 7 // If write lock thread number! 8 if (exclusiveCount(c)! = 0 && 9 getExclusiveOwnerThread() ! = current) 10 return -1; Int r = sharedCount(c); 13 /* 14 * readerShouldBlock(): whether to wait for the lock(fair lock principle) 15 * r < MAX_COUNT: 16 * compareAndSetState(c, C + SHARED_UNIT) : Set the read lock state 17 */ 18 // Whether the read thread should be blocked and less than the maximum value, and compare the setting successfully 19 if (! ReaderShouldBlock () &&20 r < MAX_COUNT &&21 compareAndSetState(c, c + SHARED_UNIT)) {22 //r == 0, 23 if (r == 0) {// first hold lock (s) = 1; 27 firstReaderHoldCount = 1; 28} else if (firstReader == current) {// firstReaderHoldCount++; 31} else {// The number of read locks is not 0 and is not the current thread 32 // get the counter 33 HoldCounter rh = cachedHoldCounter; 34 / / counter is empty or dar is not for the currently running thread dar 35 if (rh = = null | | rh. Dar! = getThreadId(current)) 48 cachedHoldCounter = rh = readHolds. Get (); 38 else if (rh.count == 0) // count = 0 41 // count+ 1 42 rh.count++; 43 } 44 return 1; 45 } 46 return fullTryAcquireShared(current); 47}Copy the code

Where sharedCount method indicates the number of threads holding the read lock, source code is as follows:

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

Note: You can get the number of locks read by shifting state 16 bits to the right, because the high 16 bits of state indicate the number of locks read, and the corresponding 16 bits indicate the number of locks write.

Read lock acquisition process is slightly more complicated than write lock, first determine whether the write lock is 0 and the current thread does not possess the exclusive lock, directly return; Otherwise, determine whether the reader thread needs to block and the number of read locks is less than the maximum value and compare the setting status successfully. If there is no read lock, set firstReader and firstReaderHoldCount for the firstReader thread. If the current thread is the first reader thread, increment firstReaderHoldCount; Otherwise, the value of the HoldCounter object corresponding to the current thread is set. The flow chart is as follows.

Note: After a successful update, the current thread reentrancecount (lines 23 to 43) is recorded in firstReaderHoldCount or readHolds(of type ThreadLocal) in the local thread copy. This is to implement the getReadHoldCount() method added in JDK1.6. This method gets the number of times the current thread re-entered the shared lock (state is the total number of threads re-entered). Adding this method makes the code a bit more complicated, but the principle is simple: If there is only one thread, you do not need to use the ThreadLocal to store the reentrant value directly into the firstReaderHoldCount member. When a second thread approaches, you need to use the ThreadLocal readHolds variable. Each thread has its own replica. Used to store its own reentrant number.

FullTryAcquireShared method:

final int fullTryAcquireShared(Thread current) { HoldCounter rh = null; for (;;) {// infinite loop // getState int c = getState(); if (exclusiveCount(c) ! If (getExclusiveOwnerThread()! // return -1; } else if (readerShouldBlock()) {// Make sure we're not acquiring read lock reentrantly if (firstReader == current) {assert firstReaderHoldCount > 0; If (rh == null) {// count is not empty // rh = cachedHoldCounter; if (rh == null || rh.tid ! GetThreadId (current)) {// the count is null or the count tid is not the current running thread tid rh = readHolds. Get (); if (rh.count == 0) readHolds.remove(); } } if (rh.count == 0) return -1; If (sharedCount(c) == MAX_COUNT) // If (sharedCount(c) == MAX_COUNT) // Throw new Error("Maximum lock count exceeded"); if (compareAndSetState(c, If (sharedCount(c) == 0) {if (sharedCount(c) == 0) { // 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

In the tryAcquireShared function, if the following three conditions are not met (whether the reader thread should be blocked, less than the maximum value, and the comparison set successfully), the fullTryAcquireShared function is used to ensure that the related operation can be successful. The logic is similar to the tryAcquireShared logic and is no longer verbose.

Read lock release, tryReleaseShared method

1 protected final Boolean tryReleaseShared(int unused) {2 thread.currentThread (); If (firstReader == current) {assert firstReaderHoldCount > 0; If (firstReaderHoldCount == 1) // If (firstReaderHoldCount == 1) 8 else // Reduce resource usage 9 firstReaderHoldCount--; 10} else {// The current thread is not the first reader thread 11 // the cache counter 12 HoldCounter rh = cachedHoldCounter; 13 if (rh == null || rh.tid ! = getThreadId(current)) // count (s) is null or not the tid of the current running thread. Int count = rh.count; 13 if (count <= 1) {13 if (count <= 1) {13 if (count <= 1) 22 Throw unmatchedUnlockException(); 22 Throw unmatchedUnlockException(); 23} 24 // reduce count 25 --rh.count; 26 } 27 for (;;) Int c = getState(); 30 int nexTC = c-shared_unit; 30 int nexTC = c-shared_unit; If (compareAndSetState(C, nexTC)) // Releasing the read lock has no effect on readers, 34 // but it may allow waiting writers to proceed if 35 // both read and write locks are now free. 36 return nextc == 0;  37}} 38Copy the code

Note: This method indicates that the lock reader thread releases the lock. FirstReaderHoldCount = 1 firstReaderHoldCount = 1 firstReaderHoldCount = 1 firstReaderHoldCount = 1 firstReaderHoldCount = 1 The number of resources held by the first reader thread, firstReaderHoldCount, is reduced by 1; If the current thread is not the first to read the thread, the first will get the cache counter (a read lock on the thread corresponding counter), if the counter is empty or dar dar value is not equal to the current thread, then get the current thread counter, if counter count count less than or equal to 1, then remove the current thread corresponding counter, If the count of a counter is less than or equal to 0, an exception is thrown and the count can then be reduced. In either case, you enter an infinite loop that ensures that the state state is set successfully. The flow chart is as follows.

There will always be an object in the process of acquiring and releasing the read lock. At the same time, the object is +1 when the thread acquires the read lock and -1 when the read lock is released. This object is the HoldCounter.

To understand a HoldCounter, you need to understand how to read locks. As mentioned earlier, the internal implementation mechanism of read lock is shared lock. For shared lock, we can think of it as not a lock concept, but more like a counter concept. A shared lock operation is equivalent to a counter operation, acquiring the shared lock counter +1 and releasing the shared lock counter -1. 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.

Read the lock to acquire the lock:

If (r == 0) {// if (r == 0) {// if (r == 0) {// if (r == 0); firstReaderHoldCount = 1; } else if (firstReader == current) {// the first read-lock thread reenters firstReaderHoldCount++; } else {// Non-firstReader count HoldCounter rh = cachedHoldCounter; //readHoldCounter Cache //rh == null or rh.tid! = current. GetId (), it is necessary to obtain rh if (rh = = null | | rh. Dar! = current.getId()) cachedHoldCounter = rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); // add rh.count++ to readHolds; // count +1}Copy the code

Why do we have a firstRead, firstRead holdcount here? Instead of just using the else part of the code, right? For efficiency reasons, firstReaders will not be placed in readHolds, and looking for readHolds will be avoided if only one lock is read. Maybe you don’t understand HoldCounter very well just by looking at this code. FirstReader, firstReaderHoldCount firstReader, firstReaderHoldCount

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

The two variables are relatively simple, one representing the thread, which of course is a special thread, and one representing the reentrant count of firstReader.

HoldCounter definition:

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

There are only two variables in a HoldCounter: count and tid, where count represents the counter and tid is the thread ID. But if you want to bind an object to a thread, it’s not enough to just record tiDs, and a HoldCounter doesn’t bind objects at all, it just records thread tiDs.

Of course, in Java, we know that only ThreadLocal can bind a thread to an object. So here it is:

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

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.

Third, summary

Through the above source code analysis, we can find a phenomenon:

If a thread holds a read lock, the thread cannot acquire a write lock (because when acquiring a write lock, if the current read lock is found to be occupied, the acquisition fails immediately, regardless of whether the read lock is held by the current thread).

If a thread holds a write lock, the thread can continue to acquire the read lock (if the write lock is occupied, it will fail to acquire the read lock only if the write lock is not occupied by the current thread).

If you think about it, this design makes sense: when a thread acquires a read lock, other threads may also be holding the read lock, so the thread that acquires the read lock cannot be “upgraded” to a write lock. The thread that acquired the write lock must have exclusive access to the read lock, so it can continue to acquire the read lock. When it has acquired both the write lock and the read lock, it can also release the write lock and continue to hold the read lock, so that a write lock “degrades” to the read lock.

In conclusion:

To hold both the write lock and the read lock, a thread must acquire the write lock first and then the read lock. Write locks can be “downgraded” to read locks; Read locks cannot be “upgraded” to write locks.

I will always share Java dry goods, also will share free learning materials courses and interview treasure book reply: [computer] [design mode] [interview] have surprise oh