ReentrantReadWriteLock – read-write lock

A ReentrantLock is an exclusive lock. Only one thread can acquire the lock at a time, but in most scenarios, the read service is available most of the time, and the write service occupies less time.

  • Read/write locks allow access by multiple lower readers at the same time, but all readers and other threads are blocked when the writer thread accesses them. Read-write locks maintain a pair of locks, read locks and write locks. By separating read locks and write locks, concurrency is greatly improved compared to the general exclusive locks.
  • Before Java 5, if you want to achieve read/write separation, you need to use Java wait notification mechanism, namely synchronized. Only after the write operation is completed and notified, all waiting read operations can continue to be executed. The purpose of this is to ensure that the read operation can read correct data and avoid dirty reads.

ReentrantReadWriteLock Implements the ReadWriteLock interface.

public interface ReadWriteLock {
    Lock readLock(a);

    Lock writeLock(a);
}
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 the customized 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. The read-write state is the synchronization state of its synchronizer. In ReentrantLock, the synchronization state is the number of times a lock is acquired by a thread. A custom synchronizer that reads or writes a lock needs to maintain the state of multiple readers and one writer on the synchronization state (an integer variable).

If you want to maintain multiple states on a single integer variable, you need to “bitwise use” this variable,Read/write locks divide this state variable into high 16 bits and low 16 bits, high 16 bits for read and low 16 bits for write.

*

The diagram above shows that a thread has acquired a write lock, re-entered it twice, and also acquired a read lock twice in a row.

Read/write locking is used to quickly determine read and write states by bit operations: assume the current synchronization state bit S, write state equal to S&0x0000FFFF (erase the high 16 bits), read state equal to S>>>16 (unsigned fill 0 right 16 bits). Write state +1 is S+1, read state +1 is S+ (1<<16).

If S is not 0 and the write status (S&0x0000FFFF) is 0, the read status is greater than 0, that is, the read lock has been obtained.

**

Write lock acquisition and release

To obtain

The write lock will eventually call the tryAcquire(int Acquires) method in Sync

// If the current thread has acquired the write lock, the write state is increased;
When the current thread acquires the write lock, the current thread enters the wait state if the read lock has already been acquired (the read status is not 0) or if the current thread acquires the write lock from another thread
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. */
    // Get the current thread
    Thread current = Thread.currentThread();
    // The current lock status
    int c = getState();
    int w = exclusiveCount(c);
    if(c ! =0) {
        // (Note: if c ! = 0 and w == 0 then shared count ! = 0)
        If c is not equal to 0 and w is equal to 0, the read lock already exists or the current thread is not the thread that has acquired the write lock
        if (w == 0|| current ! = getExclusiveOwnerThread())return false;
        // Beyond the maximum reentrant write lock range (1<<16)-1
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        setState(c + acquires);
        return true;
    }
    // Whether to block
    if(writerShouldBlock() || ! compareAndSetState(c, c + acquires))return false;
    // Set the thread that acquires the lock to the current thread
    setExclusiveOwnerThread(current);
    return true;
}
Copy the code

Similar to tryAcquire(int ARg) in ReentrantLock, read lock is added to reentrant. To ensure that write operations are visible to the read lock, if a write lock is allowed to be acquired even if the read lock is already acquired, the thread that has acquired the read lock may not be aware of the current write thread’s operations. Therefore, the write lock can only be acquired by the current thread after the read lock is released. After acquiring the write lock, all other read and write threads are blocked.

The release of

WriteLock provides the unLock() method, which calls the AQS template method and finally the tryRelease method of the internal synchronizer Sync.

protected final boolean tryRelease(int releases) {
    if(! isHeldExclusively()) {// Check that the thread currently releasing the lock is not the thread that has acquired the write lock
        throw new IllegalMonitorStateException();
    }
    int nextc = getState() - releases;
    // If the number of new threads writing a lock is zero, then the owner of the write lock is set to null (as with ReentrantLock reduction, true is not returned until all write locks are released by the current thread).
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}
Copy the code

Read lock acquisition and release

To obtain

Sync’s tryAcquireShared (**int **unused) method is used to obtain a read lock, which is a bit more complicated than a write lock:

  1. If there is a write lock and the owner of the write lock is not the current thread, false is returned
  2. If the number of threads held by the read lock is less than the maximum value (65535) and the lock status is set successfully, execute the following code (later on HoldCounter) and return 1. If the conditions are not met, run fullTryAcquireShared
 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();
     //exclusiveCount(c) : Calculates whether there is a write lock
     //getExclusiveOwnerThread() ! = current: The thread holding the write lock is not the current thread
     // Lock degradation is involved if the thread holding the write lock is the current thread
     if(exclusiveCount(c) ! =0&& getExclusiveOwnerThread() ! = current)return -1;
     // Get the status of the read lock
     int r = sharedCount(c);
     if(! readerShouldBlock() &&// If there is no need to block
         r < MAX_COUNT &&// Less than the maximum number of lock read times
         compareAndSetState(c, c + SHARED_UNIT)) {
         if (r == 0) {// The read lock has not been acquired
             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

FullTryAcquireShared is processed according to whether it needs to wait and whether the number of times for obtaining read locks exceeds the upper limit. 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.

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 (;;) {
        int c = getState();
        if(exclusiveCount(c) ! =0) {
            if(getExclusiveOwnerThread() ! = current)return -1;
            // else we hold the exclusive lock; blocking here
            // would cause deadlock.
        } else if (readerShouldBlock()) {// Read locks need to block
            // Make sure we're not acquiring read lock reentrantly
            if (firstReader == current) {// The current thread has acquired the read lock
                // assert firstReaderHoldCount > 0;
            } else {
                //HoldCounter
                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; }}// Read lock exceeds the maximum limit
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // If the CAS lock status is set successfully
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            // If it is the first time to acquire the read lock
            if (sharedCount(c) == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {FirstReaderHoldCount +1 if the current thread is the first thread to acquire the lock (firstReader)
                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 release of

TryReleaseShared (**int **unused) is eventually called in Sync.

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    if (firstReader == current) {// Determine whether the first thread to acquire the read lock is the current thread to release the lock
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1)// Set firstReader to null if it is fetched only once
            firstReader = null;
        else
            firstReaderHoldCount--;// Otherwise count -1
    } else {
        // Get the RH object and update "current thread get lock information"
        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;
    }
    // The CAS updates the synchronization status
    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

HoldCounter

A HoldCounter is used to store the number of times a thread has acquired a lock, but does not include the number of times a thread has acquired a lock for the first time. Doug Lea holds the HoldCounter separately from the thread that acquired the lock for the first time. You don’t need to use a HoldCounter; The HoldCounter uses ThreadLocl to store information for each thread

/** * A counter for per-thread read hold counts. * Maintained as a ThreadLocal; cached in cachedHoldCounter */
static final class HoldCounter {
    int count = 0;
    // 
    final long tid = getThreadId(Thread.currentThread());
}
/** * ThreadLocal subclass. Easiest to explicitly define for sake * of deserialization mechanics. */
static final class ThreadLocalHoldCounter
    extends ThreadLocal<HoldCounter> {
    public HoldCounter initialValue(a) {
        return newHoldCounter(); }}Copy the code

Bind a HoldCounter to the current thread with a ThreadLocal, and the HoldCounter also holds the thread ID. But the reason the bound thread ID is not a thread object is to facilitate GC collection.

Lock down

A thread that requests a read lock without releasing a write lock is a lock degradation supported by ReentrantReadWriteLock. You can see this in the code that acquires the read lock

  • Lock down must get read lock first, then release the write lock: access to write lock > > get read lock release the write lock, in order to guarantee the data visibility, if the current thread does not get read lock but a direct release the write lock, assuming that at this time another thread to obtain the write lock and modify the data, then the current thread can’t perceive thread T data update.
  • If the lock degradation process is followed, other writer threads will be blocked
//exclusiveCount(c) : Calculates whether there is a write lock
//getExclusiveOwnerThread() ! = current: The thread holding the write lock is not the current thread
// Lock degradation is involved if the thread holding the write lock is the current thread
if(exclusiveCount(c) ! =0&& getExclusiveOwnerThread() ! = current)return -1;
Copy the code

Lock escalation

A thread applying for a write lock without releasing the read lock is a lock upgrade. ReentrantReadWriteLock is not supported because the thread that has obtained the read lock may not see the operation of the thread that has obtained the write lock.