Scan the qr code below or search the wechat official account, cainiao Feiyafei, you can follow the wechat official account, read more Spring source code analysis and Java concurrent programming articles.

The problem

Here are a few questions to consider before reading this article

    1. What is a read/write lock?
    1. What does ReadWriteLock exist for?
    1. What scenarios do read/write locks apply to?
    1. What are lock downgrades and lock upgrades?

Introduction to the

  • The lock implemented by synchronized and ReentrantLock isExclusive lockThe exclusive lock allows only one thread to access a shared resource at a time. However, in ordinary scenarios, we usually encounter an exclusive lock for a shared resourceRead more to write lessThe scene. For read scenarios, only one thread is allowed to access a shared resource at a time. Obviously, this situation is inefficient to use exclusive locking, so how to optimize?
  • At this timeRead-write lockRead/write locking is a common technique, not unique to Java. From the name, read-write locks have two locks,Read lockandWrite lock. Read/write locks allow multiple threads to read a shared resource at the same time. Only one thread can write to a shared resource at a time. When a write operation is performed, the read operation of another thread will be blocked at the same time. When a read is performed, all threads write operations are blocked at the same time. Read locks are called shared locks because multiple threads can access and read shared resources at the same time. For write locks, only one thread is allowed to access the shared resource and write at the same time, so it is called an exclusive lock.
  • Passed in JavaReadWriteLockTo implement read/write locks. ReadWriteLock is an interface,ReentrantReadWriteLockIs a concrete implementation class for the ReadWriteLock interface. Two inner classes are defined in ReentrantReadWriteLockReadLock,WriteLock, respectively to achieve read lock and write lock. ReentrantReadWriteLock uses AQS to obtain and release locks. Therefore, ReentrantReadWriteLock defines a synchronization component that inherits the AQS classSyncAnd ReentrantReadWriteLock alsoSupport fairness and injustice, so it also defines two inner classes internallyFairSync, NonfairSyncThey inherit Sync.
  • ReentrantReadWriteLock not only provides read, write, and release locks, but also provides other methods related to lock status. See the table below (from the Art of Concurrent Programming in Java, p. 141).
The method name function
int getReadLockCount() The number of read locks is not necessarily the same as the number of read locks, because locks can be reentrant
int getReadHoldCount() Gets the number of times the current thread reenters the read lock
int getWriteHoldCount() Gets the number of times the current thread reenters the write lock
int isWriteLocked() Check whether the lock is a write lock. Returns true, indicating that the lock is a write lock

Realize the principle of

  • In AQS, passThe int typeThe global variable state is used to represent the synchronization state, which is used to represent the lock. ReentrantReadWriteLock also uses AQS to implement locks. However, ReentrantReadWriteLock has two locks: read locks and write locks. Both locks protect the same resource. The answer isAccording to a resolution.
  • Since state is a variable of type int, it is in memoryIt takes four bytes, or 32 bits. Split it into two parts: high 16 bits and low 16 bits, whereThe high 16 bits indicate the read lock status, and the low 16 bits indicate the write lock status. When the read lock is set successfully, the 16-bit height is increased by 1. When the read lock is released, the 16-bit height is decreased by 1. When the write lock is set successfully, the lower 16 bits are added by 1, and when the write lock is released, the 16th bit is subtracted by 1. As shown in the figure below.

  • How to determine whether the current lock state is written or read based on the value of state?
  • Assuming the lock’s current state value is S, add S to the hexadecimal number0x0000FFFFforWith the operation, i.e. S&0x0000FFFF, the highest 16 bits are all set to 0 during operation and the result is denoted as C, so C represents the number of write locks. If c equals 0, no thread has acquired the lock; If c is not equal to 0, it means that a thread has acquired the lock, and c equals how many times the lock has been reentered.
  • Will SUnsigned 16 bits to the right(S>>>16), the result isThe number of read locks. When S>>>16 does not equal 0 and c does not equal 0, the current thread holds both write and read locks.
  • When a read lock is successfully acquired, how do I increment the read lock by one? The result of S + (1<<16) is to add 1 to the lock. When the read lock is released, the S – (1<<16) operation is performed.
  • When the write lock is successfully obtained, S+1 indicates that the write lock status +1. When the write lock is released, an S-1 operation is performed.
  • Since both read and write locks occupy only 16 bits, the maximum number of read locks is-1, the maximum number of times that a write lock can be reentrant is1.

Source code analysis

Now that you understand how to represent lock state through state, you’ll look at the source code implementation of reading and writing locks through source code.

  • Create read and write locks with the following code. If no parameters are passed in the ReentrantReadWriteLock constructor,An unfair read/write lock is created by default. In read/write locks, stillUnfair read/write lock performance is due to fair read/write locks.
ReadWriteLock lock = new ReentrantReadWriteLock();
// Create a read lock
Lock readLock = lock.readLock();
// Create a write lock
Lock writeLock = lock.writeLock();	
Copy the code

Write locks lock

  • When the lock() method of the writeLock is called, the thread attempts to acquire the writeLock, writelock.lock (). Since a write lock is an exclusive lock, the process of acquiring a write lock is almost the same as that of ReentrantLock. When the lock() method is called, acquire() of AQS is first called, and tryAcquire() of subclasses is first called in acquire(), So the tryAcquire() method of Sync, the inner class of ReentrantReadWriteLock, is called. The source code for this method is below.
protected final boolean tryAcquire(int acquires) {
    
    Thread current = Thread.currentThread();
    int c = getState();
    // The exclusiveCount() method assigns an & to 0xFFFF. The result is the number of write locks.
    // So w means the number of write locks
    int w = exclusiveCount(c);
    // If c is not 0, the lock is occupied. At this point, you have to look at the value of w.
    // If c equals 0, the lock is not being held by any thread
    if(c ! =0) {
        // (Note: if c ! = 0 and w == 0 then shared count ! = 0)
        //
        /** * 1. If w is 0, the number of write locks is 0, and if c is not 0, the lock is occupied, but it is not a write lock, then the lock must be read lock. * If w is 0, the lock must fail to acquire the lock. The tryAcquire() method returns false. * 2. If w is not 0, write lock is in the current lock state. = getExclusiveOwnerThread() determines whether the thread holding the lock is the current thread. If it is the current thread, then proceed to the following logic. Why should the current thread hold the lock and still be able to perform subsequent logic? * Because read/write locks support reentrant. * /
        if (w == 0|| current ! = getExclusiveOwnerThread())return false;
        // The next line of code determines that the write lock will not exceed the maximum number of reentrant times, which is 2 ^ 16 minus 1
        // Why 2 to the 16th minus 1? Because the lower 16 bits of state hold write locks, the maximum number of write locks is 2 ^ 16 minus 1
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        setState(c + acquires);
        return true;
    }
    /** * 1. The writerShouldBlock() method is used to determine whether the current thread should block or not. For an unfair write lock, it will determine if there is a queue in the synchronization queue. If there is a queue, it will return true, indicating that the current thread needs to block. Return false if no one is queuing. * * 2. When writerShouldBlock() returns true, it means that the current thread cannot directly acquire the lock, so tryAcquire() returns false. * If writerShouldBlock() returns false, the current thread can attempt to acquire the lock, so the CAS method is used to try to change the value of the synchronization variable. * If the CAS method succeeds, the current thread has acquired the lock. The tryAcquire() method returns true. If the modification fails, tryAcquire() returns false, indicating that it failed to acquire the lock. * * /
    if(writerShouldBlock() || ! compareAndSetState(c, c + acquires))return false;
    setExclusiveOwnerThread(current);
    return true;
}
Copy the code
  • In the tryAcquire() method, pass firstexclusiveCount()Method to calculate the number of write locks, how to calculate? That’s the sum of state and theta0x0000FFFFforWith the operation.
  • If state is equal to 0, it means that neither the read lock nor the write lock was acquired, and the current thread is calledwriterShouldBlock()The tryAcquire() method determines whether the thread needs to wait. If it does, the tryAcquire() method returns false, indicating that it failed to acquire the lock and goes back to the AQS acquire() method with the same logic as the exclusive lock. If there is no need to wait, try to change the value of state. If the change is successful, the lock is obtained successfully. Otherwise, the lock fails.
  • If state is not equal to 0, then it means that there is a read or write lock. I’m going to have to look at the value of w.
  • If w is 0, it means that the number of write locks is 0, and c is not 0, it means that the lock is occupied, but it is not a write lock. Then the lock state must be a read lock. Since it is a read lock state, it will definitely fail to acquire the lock when the write lock exists, because it cannot obtain the write lock. So when w equals 0, the tryAcquire() method returns false.
  • If w is not 0, it indicates that the lock status is write lock. Proceedcurrent ! = getExclusiveOwnerThread()Determines whether the thread holding the lock is the current thread. If not the current thread, tryAcquire() returns false; If it is the current thread, then proceed to the following logic. Why is the current thread holding the lock able to perform the following logic? Because read/write locks are reentrant.
  • If the current thread acquires the write lock, it then determines whether the maximum number of reentrant times will be exceeded when the write lock is re-entered, and if so, it throws an exception. (Since the lower 16 bits of state represent write locks, the maximum number of times a write lock can be reentrant is1).

Write lock release

  • Write locks are released with almost the same logic as exclusive locks. When writelock.unlock () is called, the AQS release() method is called first, and the subclass tryRelease() method is called first in release(). The tryRelease() method of Sync, the inner class of ReentrantReadWriteLock, is called here. Write lock release logic is relatively simple, you can refer to the source code comments below. The source code and comments for the method are shown below.
protected final boolean tryRelease(int releases) {
    // Determine if the current thread holds the lock
    if(! isHeldExclusively())throw new IllegalMonitorStateException();
    // Subtract the state value from Releases
    int nextc = getState() - releases;
    // Call the exclusiveCount() method to count the number of write locks. If the number of write locks is 0, the write locks are released completely. In this case, set the AQS 'exclusiveOwnerThread attribute to null
    // And returns the free flag indicating whether the write lock has been fully released
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}
Copy the code

Read lock lock

  • Read locks are shared locks, so when the readLock.lock() method is called, the AQS acquiredShared() method will be called first, and in the acquireShared() method the subclass tryAcquireShared() method will be called first. The internal class Sync of ReentrantReadWriteLock will be calledtryAcquireShared()Methods. The source code for this method is below.
protected final int tryAcquireShared(int unused) {
    
    Thread current = Thread.currentThread();
    int c = getState();
    // exclusiveCount(c) Returns the number of write locks. If it is not 0, the write lock is occupied. If the thread that is holding the write lock is not the current thread, -1 is returned, indicating that the lock failed to obtain
    if(exclusiveCount(c) ! =0&& getExclusiveOwnerThread() ! = current)return -1;
    // r is the number of read locks
    int r = sharedCount(c);
    /** * the following code makes three judgments: * 1, read locks should be queued. If no one is in line, use if. FullTryAcquireShared () * 2. Check whether the number of read locks exceeds the maximum value. * 3. Try to change the value of the synchronization variable */
    if(! readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {// When the number of read locks is 0, the current thread is set to firstReader and firstReaderHoldCount=1
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            // If the number of read locks is not zero and firstReader is the current thread, the firstReaderHoldCount is accumulated
            firstReaderHoldCount++;
        } else {
            // The number of read locks is not zero, and the first thread to acquire the read lock is not the current thread
            // The number of times the current thread acquired the read lock is stored.
            ReadHolds are an instance of ThreadLocal
            HoldCounter rh = cachedHoldCounter;
            if (rh == null|| rh.tid ! = getThreadId(current)) cachedHoldCounter = rh = readHolds.get();else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        // If 1 is returned, the read lock is successfully obtained
        return 1;
    }
    // If none of the three judgments in if are satisfied, we will execute here and call the fullTryAcquireShared() method to try to get the lock
    return fullTryAcquireShared(current);
}
Copy the code
  • In the tryAcquireShared() method, it passes firstexclusiveCount()Method to calculate the number of write locks, if the write lock exists, then determine whether the thread holding the write lock is the current thread, if not the current thread, it means that the write lock is occupied by another thread, the current thread can not obtain the read lock. The tryAcquireShared() method returns -1, indicating that the read lock failed to be obtained. If the write lock does not exist or the thread holding the write lock is the current thread, then the current thread has a chance to acquire the read lock.
  • Next, it will determine whether the current thread does not need to queue for the read lock, whether the number of read locks will exceed the maximum value, and whether the state of the read lock is successfully changed through CAS (add the value of state by 1<<16). If these three conditions are true, it will enter the if statement block. This block of code is tedious, but the function is relatively simple, is to count the number of locks read and the number of times the current thread to read the lock, the underlying principle isThreadLocal. Because it is provided in the read/write lockGetReadLockCount (), getReadHoldCount ()And so on. That’s where the data comes from.
  • If one of the above conditions is not true, the if block is not entered and the fullTryAcquireShared() method is called. The function of this method is to keep the thread to acquire the lock, its source is as follows.
final int fullTryAcquireShared(Thread current) {
    /* * This code is in part redundant with that in * tryAcquireShared but is simpler overall by not * complicating tryAcquireShared with interactions between * retries and lazily reading hold counts. */
    HoldCounter rh = null;
    // for an infinite loop that returns until a condition is met, otherwise the loop continues
    for (;;) {
        int c = getState();
        // If the lock status is write lock, the thread holding the lock is not equal to that of the current thread
        if(exclusiveCount(c) ! =0) {
            if(getExclusiveOwnerThread() ! = current)return -1;
            // else we hold the exclusive lock; blocking here
            // would cause deadlock.
        } else if (readerShouldBlock()) {
            // Make sure we're not acquiring read lock reentrantly
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
            } else {
                if (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");
        // Try to set the value of the synchronization variable. Once set successfully, it means that the current thread has acquired the lock, and then set the number of times to acquire the lock and other related information
        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
  • The tryAcquireShared() method returns 1 when the read lock is successfully obtained, so when you return to the AQS acquireShared() method, it will end directly. If the lock fails to be obtained, the tryAcquireShared() method returns -1, and in AQS, the doAcquireShared() method is followed. The doAcquireShared() method simply adds itself to the synchronization queue and waits until the lock is acquired. This method does not respond to interrupts.

Read lock release

  • When the readLock.unlock() method is called, the AQS releaseShared() method is called first, and in releaseShared() the subclass’s tryReleaseShared() method is called first. The internal class Sync of ReentrantReadWriteLock will be calledtryReleaseShared()Methods. The source code for this method is below.
protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    if (firstReader == current) {
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } 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;
    }
    for (;;) {
        int c = getState();
        // Change the value of the synchronization variable (read lock state minus 1<<16)
        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
  • In the tryReleaseShared() method, the data related to the read lock count is modified, and then the CAS operation subtracts the value of state by 1<<16 in an infinite loop for. It exits from the for loop only if the CAS operation succeeds. When the number of read locks is 0, tryReleaseShared() returns true, indicating that the lock is fully released.
  • When the tryReleaseShared() method returns, the next steps are exactly the same as the shared lock release logic.

Matters needing attention

  • The use of read/write locks is very simple. However, pay attention to the following points during the use of read/write locks.
  • 1.Read-write lockLock upgrade is not supported.Support lock degradation. Lock escalation is when a thread acquires a read lock and then acquires a write lock without releasing the read lock. Lock degradation occurs when a thread acquires a write lock and then acquires a read lock without releasing the write lock. Why not support lock upgrades? You can refer to the following sample code.
public void lockUpgrade(a){
    ReadWriteLock lock = new ReentrantReadWriteLock();
    // Create a read lock
    Lock readLock = lock.readLock();
    // Create a write lock
    Lock writeLock = lock.writeLock();
    readLock.lock();
    try{
        / /... Process business logic
        writeLock.lock();   / / code (1)
    }finally{ readLock.unlock(); }}Copy the code
  • In the sample code above, if the T1 threads first get to read lock, and then execute the code, in the implementation of the code (1) on a line, T2 thread lock to get read, also due to read lock is a Shared lock, and now write lock has not been obtained, so the T2 thread is able to get to read lock, when T1 code executed to (1), to try to get to write locks, Since some thread T2 occupied the read lock, T1 could not obtain the write lock and had to wait. When T2 also executed to code ①, T1 occupied the read lock, so T2 could not obtain the write lock. Thus, the two threads kept waiting, neither acquiring the write lock nor releasing the read lock. Therefore locks do not support lock escalation.
  • Read-write locks support lock degradation, which is required for visibility. Make changes to data made by the T1 thread visible to other threads.
  • 2.Read locks do not support conditional wait queues. When the ReadLock class’s newCondition() method is called, an exception is thrown directly.
public Condition newCondition(a) {
    throw new UnsupportedOperationException();
}
Copy the code
  • Because the read lock is a shared lock, the maximum number of times is-1), can be held by multiple threads at the same time. For a read lock, there is no need for other threads to wait for the read lock, and Condition’s waiting to wake up is meaningless.

conclusion

  • This article first briefly introduces the function of read and write lock, which consists of two locks: read lock and write lock. It then introduces some of the features of read-write locks. Then it analyzes how to express the state of read/write lock through the variable state. The high 16 bits of state represent the read lock, and the low 16 bits represent the write lock0x0000FFFFforWith the operationIs the number of write locks.
  • Finally, the release and acquisition process of write lock are analyzed respectively through the source code, and the release and acquisition process of read lock. Write lock is an exclusive lock, so its release and acquisition call the exclusive lock release and acquisition method in AQS, read lock is a shared lock, so its release and acquisition call the shared lock release and acquisition method in AQS.

Related to recommend

  • Pipe program: the cornerstone of concurrent programming
  • Learn the implementation principle of CAS
  • Unsafe class source code interpretation and usage scenarios
  • Design principle of queue synchronizer (AQS)
  • Queue synchronizer (AQS) source code analysis
  • ReentrantLock source code analysis
  • Fair locks versus unfair locks
  • See the Bean creation process from the source code
  • The Spring source code series container startup process
  • In the Spring! The most! The most! Important afterprocessor! No one!!
  • @ Import and @ EnableXXX
  • Write a Redis/Spring integration plug-in by hand
  • Why should dynamic proxies in the JDK be implemented based on interfaces and not inheritance?
  • FactoryBean – one of Spring’s extension points
  • How the @autowired annotation works
  • The practical application of the one-time policy design pattern
  • How does Spring address loop dependencies
  • Concurrent programming in the Condition variable Condition source analysis