Deep understanding of readwritelock ReentrantReadWriteLock

Keywords: Java Redis MongoDB MySQL

I collated Java advanced materials for free, including Java, Redis, MongoDB, MySQL, Zookeeper, Spring Cloud, Dubbo high concurrency distributed and other tutorials, a total of 30G, which needs to be collected by myself.
Portal: https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Q

 

1. Introduction to read-write lock

1. Introduction to read-write lock

In the concurrency scenario to solve the problem of thread safety, we will almost use exclusive locks with high frequency, usually using the java keyword synchronized or the packages of concurrent to implement the Lock interface. They are all exclusive access locks, that is, only one thread can access the Lock at the same time. In some business scenarios, most of the data is only read, and few of the data is written. If the data is read only, it will not affect the data correctness (dirty read). If the exclusive Lock is still used in this business scenario, it is obvious that this will be the place where performance bottleneck occurs.

In view of this situation, java also provides another reentrantreadwritelock (read-write Lock) that implements the Lock interface. Read and write can be accessed by multiple read threads at the same time, but when the write thread accesses, all read threads and other write threads will be blocked. When analyzing the mutex of WirteLock and ReadLock, you can analyze them according to the relationship between WriteLock and WriteLock, between WriteLock and ReadLock, and between ReadLock and ReadLock. For more information about the features of read-write locks, you can see the introduction of the source code (the best way to learn when reading the source code, I am also learning, and I would like to share with you). Here is a summary:

  1. Fairness choice: support unfairness (default) and fair lock acquisition, throughput or unfairness is better than fairness;
  2. Reentry: it supports reentry, which can be acquired again after the read lock is acquired. After the write lock is acquired, the write lock can be acquired again, and the read lock can be acquired at the same time;
  3. Lock degradation: follow the order of acquiring write lock, acquiring read lock and releasing write lock. Write lock can be degraded to read lock

In order to fully understand the read-write lock, we must be able to understand the following questions: 1. How does the read-write lock record the read-write status separately? 2. How is the write lock obtained and released? 3. How is the read lock acquired and released? With these three questions, let's learn more about the read-write lock.

2. Write lock details

2.1. Acquisition of write lock

The implementation of synchronization component aggregates the synchronizer (AQS), and implements the synchronization semantics of synchronization component by rewriting and rewriting the method in the synchronizer (AQS). The underlying implementation analysis of AQS can. Therefore, write lock is still implemented in this way. At the same time, the write lock cannot be acquired by multiple threads. It is obvious that the write lock is exclusive, and the synchronization semantics of the write lock is realized by rewriting the tryAcquire method in AQS. The source code is:

protected final boolean tryAcquire(int acquires) {
    /*
     * Walkthrough:
     * 1\. If read count nonzero or write count nonzero
     *    and owner is a different thread, fail.
     * 2\. If count would saturate, fail. (This can only
     *    happen if count is already nonzero.)
     * 3\. Otherwise, this thread is eligible for lock if
     *    it is either a reentrant acquire or
     *    queue policy allows it. If so, update state
     *    and set owner.
     */
    Thread current = Thread.currentThread();
    // 1\. Get the current synchronization status of the write lock
    int c = getState();
    // 2\. Number of times to acquire a write lock
    int w = exclusiveCount(c);
    if (c != 0) {
        // (Note: if c != 0 and w == 0 then shared count != 0)
        // 3.1 When the read lock has been acquired by the read thread or the current thread is not the thread that has acquired the write lock
        // Current thread failed to acquire write lock
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        // 3.2 The current thread obtains a write lock and supports repeated locking
        setState(c + acquires);
        return true;
    }
    // 3.3 The write lock is not acquired by any thread. The current thread can acquire the write lock
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

 

Please refer to the note for the logic of this code. Here is a place to focus on, the exclusiveCount(c) method. The source code of this method is:

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

 

Among them, exclusive mask is: static final int exclusive mask = (1 < < shared shift) - 1; exclusive mask is 1 shift left 16 bits and then subtract 1, which is 0x0000FFFF. The exclusiveCount method is to match the synchronization state (state is int type) with 0x0000FFFF, that is, to take the lower 16 bits of the synchronization state. So what do the lower 16 bits represent? According to the comment of exclusiveCount method, the number of exclusive acquisition is the number of acquisition of write lock. Now we can get a conclusion that the lower 16 bits of synchronization state are used to represent the acquisition of write lock. At the same time, there is another method worthy of our attention:

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

 

This method is to obtain the number of times that the read lock is acquired. It is to move the synchronization state (int c) 16 times to the right, that is, to take the high 16 bits of the synchronization state. Now we can draw another conclusion that the high 16 bits of the synchronization state are used to indicate the number of times that the read lock is acquired. Do you still remember the first question we need to understand at the beginning? How the read-write lock records the status of the read lock and the write lock respectively? Now we have figured out the answer to this question. The schematic diagram is as follows:

 

Now let's go back to tryAcquire, the write lock acquisition method. Its main logic is: when the read lock has been acquired by the read thread or the write lock has been acquired by other write threads, the write lock acquisition fails; otherwise, the acquisition succeeds, supports reentry, and increases the write state.

2.2. Release of write lock

The tryRelease method of AQS is rewritten for write lock release. The source code is:

protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //1\. Sync state minus write state
    int nextc = getState() - releases;
    //2\. Whether the current write status is 0 or not. If it is 0, the write lock will be released
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    //3\. Update synchronization status if it is not 0
    setState(nextc);
    return free;
}

 

For the implementation logic of the source code, please see the comments. It is not difficult to understand that it is basically consistent with ReentrantLock. Here, we need to pay attention to reducing the write state int nextc = getState() - releases. The reason why we only need to directly subtract the write state from the current synchronization state is that the write state we just mentioned is represented by the lower 16 bits of the synchronization state.

3. Read lock details

3.1. Acquisition of read lock

After reading the write lock, let's look at the read lock. The read lock is not an exclusive lock, that is, the lock can be acquired by multiple read threads at the same time, which is a shared lock. According to the previous introduction to AQS, to realize the synchronization semantics of shared synchronization components, we need to rewrite the tryAcquireShared method and tryreleased shared method of AQS. The method of obtaining read lock is as follows:

protected final int tryAcquireShared(int unused) {
    /*
     * Walkthrough:
     * 1\. If write lock held by another thread, fail.
     * 2\. Otherwise, this thread is eligible for
     *    lock wrt state, so ask if it should block
     *    because of queue policy. If not, try
     *    to grant by CASing state and updating count.
     *    Note that step does not check for reentrant
     *    acquires, which is postponed to full version
     *    to avoid having to check hold count in
     *    the more typical non-reentrant case.
     * 3\. If step 2 fails either because thread
     *    apparently not eligible or CAS fails or count
     *    saturated, chain to version with full retry loop.
     */
    Thread current = Thread.currentThread();
    int c = getState();
    //1\. If the write lock has been acquired and the thread acquiring the write lock is not the current thread, the current
    // Thread failed to get read lock return-1
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    int r = sharedCount(c);
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        //2\. Current thread obtains read lock
        compareAndSetState(c, c + SHARED_UNIT)) {
        //3\. The following code mainly includes some new functions, such as getReadHoldCount()Method
        //Returns the current number of times to acquire a read lock
        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;
    }
    //4\. Processing in the second step CAS Operation failed spins are reentrant
    return fullTryAcquireShared(current);
}

 

For the logic of the code, please see the note. It should be noted that when the write lock is acquired by other threads, the read lock acquisition fails. Otherwise, the acquisition succeeds in using CAS to update the synchronization status. In addition, the current synchronization state needs to be added with shared unit ((1 < < shared shift) i.e. 0x00010000). This is because the high 16 bits of the synchronization state mentioned above are used to indicate the number of times the read lock is acquired. If CAS fails or the thread that has acquired the read lock obtains the read lock again, it is realized by the fullTryAcquireShared method. This code will not be expanded. You can have a look.

3.2. Release of read lock

The implementation of read lock release is mainly through the method tryrelease shared. The source code is as follows. See the note for the main logic:

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    // In order to realize getReadHoldCount And other new functions
    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();
        // Read lock release subtracts the read state from the synchronization state
        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;
    }
}

 

4. lock down

The read-write lock supports lock degradation, following the order of acquiring the write lock, acquiring the read lock and releasing the write lock. The write lock can be degraded into a read lock and does not support lock upgrading. The following example code about lock degradation is extracted from the ReentrantWriteReadLock source code:

void processCachedData() {
        rwl.readLock().lock();
        if (!cacheValid) {
            // Must release read lock before acquiring write lock
            rwl.readLock().unlock();
            rwl.writeLock().lock();
            try {
                // Recheck state because another thread might have
                // acquired write lock and changed state before we did.
                if (!cacheValid) {
                    data = ...
            cacheValid = true;
          }
          // Downgrade by acquiring read lock before releasing write lock
          rwl.readLock().lock();
        } finally {
          rwl.writeLock().unlock(); // Unlock write, still hold read
        }
      }

      try {
        use(data);
      } finally {
        rwl.readLock().unlock();
      }
    }
}

 

Posted by the_last_tamurai on Tue, 26 Nov 2019 23:52:01 -0800