The core concept of ReentrantReadWriteLock is as follows: Multiple read operations can be performed simultaneously. Read and write operations are mutually exclusive (read and write operations cannot be written). It also provides read and write performance. I highly recommend understanding ReentrantLock first and then ReentrantReadWriteLock. Because the logic of ReentrantLock is simpler and much of the logic (and even code) of ReentrantReadWriteLock and ReentrantLock is the same, Learning ReenTrantreadWrite Ock will be much easier once you are familiar with the ReentrantLock principle.

1 Core Attributes

For ReentrantReadWriteLock, the locks controlling read and write are separate; The code looks like this:

/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** Performs all synchronization mechanics */
final Sync sync;

public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock  readLock( { return readerLock; }
Copy the code

2. Constructors

Here is the constructor for ReentrantReadWriteLock, and you can see that the default is unfair.

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

3 Obtain the read lock

The following conditions must be met to obtain a read lock:

  • No thread holds a write lock (the state variable of AQS is 0 16 bits lower); Or the current thread owns the write lock (this is actually a write lock demoted to a read lock). That is, when one thread holds the write lock, other threads cannot acquire the read lock. This condition makes read locks and write locks mutually exclusive.
  • The readerShouldBlock() method returns false, that is, does not block to acquire the read lock.
  • The current total number of read locks is less than MAX_COUNT(2^ 16-1, or 65535). The lower 16 bits of state indicate the write lock status and the higher 16 bits indicate the read lock status.
  • A successful CAS operation increases read lock occupancy by +1(AQS state height 16 bits plus 1).

Access to read lock sample: reentrantReadWriteLock readLock (). The lock ().

3.1 the lock

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

Note: sync is ReentrantReadWriteLock# sync

3.2 acquireShared

/**
 * Acquires in shared mode, ignoring interrupts.  Implemented by
 * first invoking at least once {@link #tryAcquireShared},
 * returning on success.  Otherwise the thread is queued, possibly
 * repeatedly blocking and unblocking, invoking {@link
 * #tryAcquireShared} until success.
 *
 * @param arg the acquire argument.  This value is conveyed to
 *        {@link #tryAcquireShared} but is otherwise uninterpreted
 *        and can represent anything you like.
 */
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

Copy the code

3.3 tryAcquireShared

Attempts to obtain the lock. -1 is returned when obtaining the lock fails. The main logic of this method is as follows:

  • If another thread has acquired the write lock, and the write lock is not acquired by the current thread, obtaining the read lock fails. Ensure that read and write locks are mutually exclusive.
  • If the current lock count reaches MAX_COUNT, the read lock fails to be obtained.
  • Use readerShouldBlock to determine if the current reader thread should block. The readerShouldBlock method is described in more detail later.
  • Reentrancy means that class ThreadLocalHoldCounter extends ThreadLocal ThreadLocal holds the number of times the lock has been reentranced.
  • Then call the fullTryAcquireShared method to manipulate the number of times the current thread has acquired the lock. You can think of fullTryAcquireShared as a failed retry version of tryAcquireShared. See the fullTryAcquireShared method for detailed code analysis.
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(); if (exclusiveCount(c) ! = 0 && getExclusiveOwnerThread() ! = current) return -1; int r = sharedCount(c); // readerShouldBlock For a fair lock, return false if there is a node waiting in the queue. For unfair locks if (! readerShouldBlock() && r < MAX_COUNT && 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 ! = getThreadId(current)) cachedHoldCounter = rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); rh.count++; } return 1; } return fullTryAcquireShared(current); }Copy the code

3.4 fullTryAcquireShared

/** * Full version of acquire for reads, that handles CAS misses * and reentrant reads not dealt with in tryAcquireShared. */ 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) ! If the thread is not currently holding the write lock, return -1; If the current thread owns the write lock, you can try to acquire the read lock operation. if (getExclusiveOwnerThread() ! = current) return -1; // else we hold the exclusive lock; Blocking here // Would cause deadlock.} else if (readerShouldBlock()) { // Make sure were not read lock reentrantly if (firstReader == current) {// The current thread is the first thread to acquire the read lock. Do nothing // assert firstReaderHoldCount > 0; If (rh == null) {rh = cachedHoldCounter; if (rh == null) {rh = cachedHoldCounter; if (rh == null || rh.tid ! = getThreadId(current)) { rh = readHolds.get(); if (rh.count == 0) readHolds.remove(); }} // Need to block and non-reentrant (did not acquire read lock), acquire failed. 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) {if (sharedCount(c) == 0) { So update firstReader information firstReader = current; firstReaderHoldCount = 1; } else if (firstReader == current) {count +1 firstReaderHoldCount++; If (rh == null) rh = cachedHoldCounter; 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

3.5 readerShouldBlock

For fair locks, determine whether there are other threads in the current queue waiting to acquire the lock. If so, it should be blocked. For an unfair lock, it is determined whether the first waiting node in the current queue applies for a write lock. If it is a write lock, it should be blocked.

3.5.1 track of fair lock

For fair locks, return true if the queue is not empty and another thread is waiting in front of the current thread. That is, if another thread is waiting for the lock before the current thread, return true, that is, should block. The source code is as follows:

final boolean readerShouldBlock() { return hasQueuedPredecessors(); Public final Boolean hasqueuedBoomtime () {Node t = tail;} public final Boolean hasqueuedBoomboomtime () {Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h ! = t && ((s = h.next) == null || s.thread ! = Thread.currentThread()); }Copy the code

3.5.2 Unfair Lock

For an unfair lock, it is actually to determine whether the first waiting node in the current queue applies for a write lock. If it is a write lock, the read operation should be blocked. How to Understand: You can imagine, if the current thread for A read lock, then thread B apply to write lock, and then to apply many threads read lock, because read lock is Shared, so may lead to write lock consistent unable to get to the obstruction, here is in order to solve this problem, so when it has been A write lock waiting for, let the subsequent read lock for blocking operation.

final boolean readerShouldBlock() { return apparentlyFirstQueuedIsExclusive(); } // the queue is not empty, and the first waiting node in the queue is a write lock, then return true; Otherwise return false. final boolean apparentlyFirstQueuedIsExclusive() { Node h, s; // the queue is not empty, and the first waiting node in the queue is a write lock, then return ture (h = head)! = null && (s = h.next ! = null && ! s.isShared( && s.thread ! = null; }Copy the code

3.6 doAcquireShared

The current thread is queued for a read lock. The main logic is:

  • Create a SHARED node and add it to the queue.
  • Then, in an infinite loop, try to do the following until the lock is acquired or woken up because the lock was unacquired.
  • If node is the first waiting thread in the queue, it attempts to acquire a read lock and updates the head of the queue. If the next node is also waiting for a read lock, it updates the thread of the next node. This process implements read lock sharing.
  • Determine whether the thread corresponding to the current node should be suspended, and if so, suspend the thread through LockSupport.
  • After the thread is woken up, set the interrupt flag bit.

The code is as follows:

/** * Acquires in shared uninterruptible mode. * @param arg the acquire argument */ private void doAcquireShared(int arg) { final Node node = addWaiter(Node.SHARED); boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); If (p == head) {// If (p == head) {// If (p == head) {// If (p == head) {// If (p == head) { int r = tryAcquireShared(arg); If (r >= 0) {if (r >= 0) {if (r >= 0) {if (r >= 0) {if (r >= 0) {if (r >= 0) {if (r >= 0) {if (r >= 0) {if (r >= 0) {if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; // help GC if (interrupted) selfInterrupt(); failed = false; return; }} // Check whether the thread should be suspended after the read lock fails. If so, Then through LockSupport hung thread if (shouldParkAfterFailedAcquire (p, node) && parkAndCheckInterrupt ()) interrupted = true; } } finally { if (failed) cancelAcquire(node); }}Copy the code

4 Obtain the write lock

Obtaining a write lock meets the following conditions:

  • No thread currently occupies the read lock, i.e. (AQS state high 16 bits is 0); The write lock is not occupied (state 16 bits lower than 0) or the thread holding the write lock is the current thread. This condition ensures that read locks and write locks are mutually exclusive and write locks are reentrant.
  • The writerShouldBlock() method returns false, that is, does not block the writer thread
  • The current write lock reentrant count is less than the maximum (2^ 16-1), otherwise an Error is raised
  • Write lock state +1 via CAS race (lower state by 16 bits +1)

Access to read lock example: reentrantReadWriteLock. WriteLock (). The lock (). The process of acquiring a write lock is basically the same as the process of acquiring a lock in ReentrantLock, so it will be covered briefly below.

4.1 the lock

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

4.2 acquire

public final void acquire(int arg) { if (! tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }Copy the code

The lock is acquired exclusively, and if it fails to be acquired, the lock is queued.

4.3 tryAcquire

Please refer to ReentrantLock for details.

4.4 acquireQueued

Please refer to ReentrantLock for details.

5 Release the read lock

5.1 unlock

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

5.2 releaseShared

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
Copy the code

Release the lock, and if all read locks are released, perform the doReleaseShared logic.

5.3 tryReleaseShared

Two main things will be accomplished:

  • Set the number of read locks held by the thread.
  • Release the read lock held by the thread and retry this operation until it succeeds.

Returns true when all read locks have been released.

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();
        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

5.4 doReleaseShared

After the read lock is released, use this method to wake up the node following the next header or set the header node to PROPAGATE (PROPAGATE), that is:

  • If the header node waitStatus=SIGNAL, set the header node waitStatus=0 and wake up the thread corresponding to the node following the header.
  • If the header waitStatus=0, then set the header node waitStatus= PROPAGATE
/** * Release action for shared mode -- signals successor and ensures * propagation. (Note: For exclusive mode, release just amounts * to calling unparkSuccessor of head if it needs signal.) */ private void doReleaseShared() { /* * Ensure that a release propagates, even if there are other * in-progress acquires/releases. This proceeds in the usual * way of trying to unparkSuccessor of head if it needs * signal. But if it does not, status is set to PROPAGATE to * ensure that upon release, propagation continues. * Additionally, we must loop in case a new node is added * while we are doing this. Also, unlike other uses of * unparkSuccessor, we need to know if CAS to reset status * fails, if so rechecking. */ for (;;) { Node h = head; if (h ! = null && h ! = tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (! compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases unparkSuccessor(h); } else if (ws == 0 && ! compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head // loop if head changed break; }}Copy the code

6 Release the write lock

The release process is basically the same as the lock release process in ReentrantLock, which is not covered here.