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