ReentrantReadWriteLock Reads and writes mutually exclusive

Immersed in the busy reality, there is no time and energy to miss the past, success will not be too far away.

– screamo

“This is the third day of my participation in the First Challenge 2022. For details: First Challenge 2022”

Code case

public class ReentrantWriteReadLockDemo {
    public static void main(String[] args) {
        // Define a read/write lock
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        // Define a read lock
        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        // define a write lock
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
        new Thread(() -> {
            try {
                Thread.currentThread().setName("Thread-1");
                readLock.lock();
                System.out.println(Thread.currentThread().getName() + "Got the read lock.");
                Thread.sleep(10 * 1000);
                readLock.unlock();
                System.out.println(Thread.currentThread().getName() + "Release read lock");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
        try {
            Thread.sleep(5 * 1000);

        } catch (Exception e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            try {
                Thread.currentThread().setName("Thread-2");
                System.out.println(Thread.currentThread().getName() + "Want to get write lock");
                writeLock.lock();
                System.out.println(Thread.currentThread().getName() + "Got the write lock.");
                Thread.sleep(10 * 1000);
                writeLock.unlock();
                System.out.println(Thread.currentThread().getName() + "Release write lock");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
        try {
            Thread.sleep(20 * 1000);
        } catch(Exception e) { e.printStackTrace(); }}}Copy the code

Results output

Scenario 1: Reads and writes are mutually exclusive

Read lock source code analysis

Look at the constructors first

// Define a read/write lock
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

public ReentrantReadWriteLock(a) {
    this(false);
}
// An unfair lock is created by default
public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}
Copy the code

Readlock. lock(); readlock. lock(); readLock (); Click in and see

// Get read lock
public void lock(a) {
    sync.acquireShared(1);
}

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}
Copy the code

TryAcquireShared (arG) is an important method

protected final int tryAcquireShared(int unused) {
	// Get the current thread
    Thread current = Thread.currentThread();
    // Get the value of state in AQS, which is 0, because no one else is getting the lock
    int c = getState(); 
    if(exclusiveCount(c) ! =0&& getExclusiveOwnerThread() ! = current)return -1;
    int r = sharedCount(c);
    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

Look at this’ exclusiveCount(c) ‘, how do you compute it

static final int SHARED_SHIFT   = 16;
// The number 1 moves 16 bits to the left to become binary 1 0000 0000 0000 65536 decimal
// After subtracting 1, it becomes binary 1111 1111 1111 1111 1111 65,535
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// c is 0 bit & is 0000 0000 0000 1111 1111 1111 1111
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
Copy the code

So let’s go inside sharedCount(c), and see how this evaluates, okay

static final int SHARED_SHIFT   = 16;
// 0 unsigned 16 bits to the right is 0
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
Copy the code

So r is equal to 0, and look at the logic behind that

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;
}
Copy the code

See how this is updated compareAndSetState(c, c + SHARED_UNIT)

static final int SHARED_SHIFT   = 16;
// 1 left 16 bits 1 0000 0000 0000 65536 decimal
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);

// Update the state value
compareAndSetState(c, c + SHARED_UNIT)
Copy the code

The value of state is int and the value of type int is 4 bytes, one byte is 8 bits, so an int is 32 bits. The clever thing about read locks and write locks is that they use the same value of state, 16 bits higher for read locks, 16 bits lower for write locks, At this point the state value is 0000 0000 0000 0001 0000 0000 0000 0000, so proved that have now been added a read lock, continue to look down

// Set the current first read-lock thread
firstReader = current;
/ / the number of
firstReaderHoldCount = 1;
Copy the code

Then it returns 1, and the lock is successful

Analyze the scenario where a write lock is acquired and the read lock has not been released

writeLock.lock();

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

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

At first glance, the tryAcquire(ARG) method looks a lot like ReentrantLock

protected final boolean tryAcquire(int acquires) { / / acquires is 1
    // Get the current thread
    Thread current = Thread.currentThread();
    // Get the current value of state. The value of state has been acquired by another thread's read lock and modified to 65536
    0000 0000 0000 0000 0000 0000 0000
    int c = getState();
    
    int w = exclusiveCount(c);
    if(c ! =0) {
        // (Note: if c ! = 0 and w == 0 then shared count ! = 0)
        if (w == 0|| current ! = getExclusiveOwnerThread())return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        setState(c + acquires);
        return true;
    }
    if(writerShouldBlock() || ! compareAndSetState(c, c + acquires))return false;
    setExclusiveOwnerThread(current);
    return true;
}
Copy the code

ExclusiveCount (c)

static final int SHARED_SHIFT   = 16;
// The number 1 moves 16 bits to the left to become binary 1 0000 0000 0000 65536 decimal
// After subtracting 1, it becomes binary 1111 1111 1111 1111 1111 65,535
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// c is 65536 bits & 65536 bits & 65535 bits
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000, 0000,
// 所以 exclusiveCount(65536) = 0 
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
Copy the code

If w is 0 but c is not 0, then the read lock is set, because state 16 bits high is not 0

if(c ! =0) {
    // w is 0 but c is not 0
    if (w == 0|| current ! = getExclusiveOwnerThread())return false;
    if (w + exclusiveCount(acquires) > MAX_COUNT)
        throw new Error("Maximum lock count exceeded");
    // Reentrant acquire
    setState(c + acquires);
    return true;
}
if(writerShouldBlock() || ! compareAndSetState(c, c + acquires))return false;
setExclusiveOwnerThread(current);
return true;
Copy the code

AcquireQueued (addWaiter(Node.exclusive), ARG) acquireQueued(addWaiter(Node.exclusive), ARG

private Node addWaiter(Node mode) {
    // Create a node
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if(pred ! =null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}
Copy the code

Creates a node that sets the thread and mode of the current node

Node(Thread thread, Node mode) {     // Used by addWaiter
    this.nextWaiter = mode;
    this.thread = thread;
}
Copy the code



We define a pred variable that points to tail, which points to null. Tail is the last node in the AQS queue, so perd is also null

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                returnt; }}}}Copy the code

ReentrantLock (); / / compareAndSetHead(new Node()); / / ReentrantLock (); This method creates an empty head node, then the head pointer points to the empty head node, and tail points to the empty node.

private final boolean compareAndSetHead(Node update) {
    return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
Copy the code



In the next loop, tail pointing to t is no longer null, so else logic is used to point the precursor of the newly enqueued node to t



Try placing the tail pointer to the newly enqueued node and, if successful, next to the T pointer to the newly enqueued node



AcquireQueued (addWaiter(Node.exclusive), ARG) method

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // Get the precursor node of the current node
            final Node p = node.predecessor();
            // Now p is pointing to the head node, so try again to acquire the lock
            // If it succeeds, node is set as the new head node, but the previous thread does not have one yet
            // Release the lock, so it failed again
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true; }}finally {
        if(failed) cancelAcquire(node); }}Copy the code

Come this way shouldParkAfterFailedAcquire (p, node)

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // The first time the waitStatus is null,
    // So the first time we set waitStatus of this header to SIGNAL (-1) and return false
    // Repeat the above for loop again, the above process again through the lock failed, then the second time
    // Now the waitStatus of the head node is not 0 but -1, so it returns true
    int ws = pred.waitStatus; 
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
Copy the code



Return true on success to pass the thread of the current node through LockSupport.park(this); It’s suspended. At this point, the scenario analysis of read lock and write lock is complete, now if the read lock release lock, continue analysis.

Read lock release lock source code analysis

readLock.unlock();

public void unlock(a) {
    sync.releaseShared(1);
}

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

Take a look at the tryReleaseShared(ARG) method that tries to release the lock

protected final boolean tryReleaseShared(int unused) { // unused = 1 
    // Get the current thread
    Thread current = Thread.currentThread();
    FirstReaderHoldCount == 1 and firstReaderHoldCount == 1
    FirstReader = null firstReader = null
    // If firstReaderHoldCount is not 1 then firstReaderHoldCount-- the number of locks held by the reader thread -- is reduced by 1
    // This can be caused by a thread adding too much read lock
    if (firstReader == current) {
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } else {
        // If it is not the current thread, get the number of read lock threads currently held
        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();
        }
        // The number of read lock threads decreases by 1
        --rh.count;
    }
    // The core code is still there
    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

The top piece of code is not core code, let’s analyze the bottom piece of code, this piece of code is core code

for (;;) {
    // Get the current state value, 65536 (binary)
    // 0000 0000 0000 0001 0000 0000 0000 0000
    int c = getState();
    // Calculate the value of state
    // SHARED_UNIT is (1 << SHARED_SHIFT)
    // 65536 = 0
    int nextc = c - SHARED_UNIT; / / 0
    if (compareAndSetState(c, nextc))
        / / return true
        return nextc == 0; 
}
Copy the code

DoReleaseShared ()

for (;;) {
    // define an h to point to the head node
    Node h = head;
    if(h ! =null&& h ! = tail) {// Signal is -1
        int ws = h.waitStatus;
        if (ws == Node.SIGNAL) {
            // Change the waitStatus of the head node to 0
            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



The head node pointed to by H is not empty, nor is it a tail node because tail points to the node that was joined before, the head node signal is -1, then change the waitStatus of the head node to 0, the unparkprecursor (H) is well known, So this is the wake up node. Let’s go inside

private void unparkSuccessor(Node node) { // The header node is passed in
    int ws = node.waitStatus; // Now waitStatus is 0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    // s refers to the next node from the head node
    Node s = node.next;
    // s is not null,
    if (s == null || s.waitStatus > 0) {
        s = null;
        for(Node t = tail; t ! =null&& t ! = node; t = t.prev)if (t.waitStatus <= 0)
                s = t;
    }
    // Wake up
    if(s ! =null)
        LockSupport.unpark(s.thread);
}
Copy the code



Locksupport. unpark(s.read) wakes up the node thread that blocked when enqueued.

What does the lock writer thread do when it wakes up

Let’s see where the thread is suspended. Is the thread suspended in the figure below



If the lock is acquired successfully, use the setHead method to turn the awakened node into the head node

private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}
Copy the code

The head pointer points to the node node, the node node becomes NULL, and the node precursor becomes NULL, as shown in the following figure



And then p.ext is empty, so this is garbage collection



The write lock is now successfully added