“What will be hot and what to learn in 2022? This article is participating in the” Talk about 2022 Technology Trends “essay campaign.
Multithreading has always been my nightmare, and I have been looking at this knowledge recently;
But I don’t know how to write articles about multithreading, so I’m going to share the basic ReentrantLock with you while I’m sorting out my notes.
So with this article, the article has written the wrong place hard everybody big guy pointed out
Basic knowledge of
ReentrantLock is classified into fair and unfair locks.
UnLock obtains a lock. UnLock releases a lock.
Note1 :(bidirectional linked list. Both head and tail point to Null when initialized, and then an empty Node is created when a new Node is added. Both head and tail point to the empty Node to indicate that the initialization is complete.
Queue entry moves forward by moving the tail node, and queue exit moves forward by moving the head node. When the Head Node and tail point to the same Node, it means that there is no blocking thread in the queue.
If the Node thread is in the waiting state, the Node thread can be woken up by using the Unpark command provided by LockSupport.
If a block is awakened, tryAcquire() will be invoked to see if the previous node is Head (Head is always Null). If so, tryAcquire() will be invoked in the same order as the blocking queue.
State is used to record the state of the lock :(0 means that no thread is holding the lock, 1 means that there is a thread holding the lock, 2 means that the lock holding the thread has acquired the lock again, that is, re-entered the lock)
NonFair will first perform a CAS to change the state of the lock directly. , will enter the AQS acquire logic after the failure
The lock NonFair
final void lock(a) {
if (compareAndSetState(0.1)) // Modify state directly to enter AQS acquire logic
setExclusiveOwnerThread(Thread.currentThread()); // If state is changed successfully, no thread holds the lock.
// And represents the setting to record the current thread (in preparation for reentrant later)
else{
acquire(1);
}
Copy the code
Fair of the lock
final void lock(a) {
acquire(1);// Do not try to get the lock directly, go directly into AQS acquire logic
}
Copy the code
The AQS acquire
Acquire, acquireInterruptibly (AQS); acquire, acquireInterruptibly
AQS Acquire source code:
public final void acquire(int arg) {
if(! tryAcquire(arg) &&//teyAcquire has different logic in NonFair and Fair. Represents an attempt to acquire a lock
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){ //addWaiter is used to wrap the current thread as a Node and add it to the blocking queue of AQS. The subsequent acquireQueued is the blocking thread. When the thread wakes up and takes the lock, it checks whether the blocking has been interrupted.
AcquireQueue returns true if an interrupt occurred, false if no interrupt occurred. SelfInterrupt performs compensation processing
selfInterrupt()
}
Copy the code
SelfInterrupt source
static void selfInterrupt(a) {
Thread.currentThread().interrupt(); // make amends
}
Copy the code
Code explanation:
First,tryAcquire will be attempted, and the logic for tryAcquire to acquire locks based on Faire and NonFair is also different.
1.1 NonFair first checks if the lock has been acquired by a thread (state>0) : if so, it checks if the thread that acquired the lock is the current thread: if CAS changes state +1, return true (reentrant); Return false if it is not the current thread. If the lock has not yet been acquired by another thread, cas changes the status to 1 to indicate that the thread has acquired the lock once, and then stores the thread that acquired the lock.
Fair checks whether the lock has already been acquired by a thread (state>0). If so, it checks whether the thread that acquired the lock is the current thread: true if cas changes state +1; Return false if not the current thread. If the lock has not been acquired by another thread, also check that it is not currently blocking the head of the queue in the AQS. It will return false if it is not in the header
Only threads that block at the head of the queue will have their CAS status changed to 1 to indicate that the thread acquired the lock once, and then store the thread that acquired the lock.
NonFair tryAcquire source code:
final boolean nonFairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {// If no thread is holding the lock, try changing state directly
setExclusiveOwnerThread(current);// Successful modification means that the lock is successfully acquired and the current thread is saved
return true; }}else if (current == getExclusiveOwnerThread()) {// The lock is already held by the current thread. If the lock is held by the current thread, the state can be reentered and added by one.
int nextc = c + acquires; // Change state+1 to reentrant state2
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);// Set the state.
return true; // Return true to obtain the lock successfully
}
False: failed to acquire the lock
return false;
}
Copy the code
FairTyrAcquire source code:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if(! hasQueuedPredecessors() && compareAndSetState(0, acquires)) { // This condition is more stringent than NonFair. If no thread holds the lock, the current thread must be checked to see if it is at the head of the AQS blocking queue. The state value can be changed only at the head of the queue, otherwise it cannot be changed. Note: This is also the difference between a fair lock and an unfair lock
setExclusiveOwnerThread(current);// To be fair, block queue in order
return true; }}else if (current == getExclusiveOwnerThread()) {// If the thread has already acquired the lock, it can directly reenter the state
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
Copy the code
Second, if the attempt to acquire the lock is successful, the thread can continue running
(The first case for NonFair is that there is no thread currently holding the lock, and the second case is a rush. For Fair, the first case is a rush and the second case is no thread holding the house and the current thread is blocking the head of the queue.)
2.1 acquireQuened(addWaiter()) is called if the attempt to obtain the lock fails. AddWaiter () first wraps the current thread as a Node and then adds it to the blocking queue in the AQS via cas (see Note1)
2.2 addWaiter() is used to add a thread to the AQS blocking queue and then acquireQueued to block the thread.
Blocking and wakeup refer to the park and unPark classes, where unPark wakes specified threads, rather than Object’s wait and notiify.
AddWaiter source :(returns the added Node)
/**
* Creates and enqueues node for current thread and given mode. *
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
// Encapsulate the current thread as a Node constantly use cas to point the tail Node to the current Node. That is, join the team operation
private Node addWaiter(Node mode) {
Node node = new Node(mode);
for (;;) {
Node oldTail = tail;
if(oldTail ! =null) {
U.putObject(node, Node.PREV, oldTail);
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
returnnode; }}else{ initializeSyncQueue(); }}}/** * Initializes head and tail fields on first contention. */Initialize the tailtail
private final void initializeSyncQueue(a) {
Node h;
if (U.compareAndSwapObject(this, HEAD, null, (h = new Node())))
tail = h;
}
/**
* CASes tail field.
*/// Keep setting tail to point to the current Node
private final boolean compareAndSetTail(Node expect, Node update) {
return U.compareAndSwapObject(this, TAIL, expect, update);
}
Copy the code
2.3 Before blocking, the logic of obtaining the lock will be tried once. If the lock is not obtained, park will be called to block itself.
Note: Since park blocks, interrupt also wakes up in addition to unPark. At this point, it depends on whether an interrupt is immediately responded to or handled.
AcquireQuened source
// The previous step addWaiter means to add to the blocking queue. This step will block the thread.
final boolean acquireQueued(final Node node, int arg) {
try {
Since acquire does not immediately respond to interrupt during blocking, the identity record is required. The thread compensates when it acquires the lock
boolean interrupted = false;
// Purpose of the loop: Since park is woken up by interrupt, it is necessary to check if unPark is not woken up and continue blocking.
for (;;) {
// Fetch the previous Node to see if it is a Head Node (Head points to an empty Node Node). If the previous Node is Head, it means that the current Node is the first Node to exit the queue. If the previous Node is Head, it means that the lock can be acquired.
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {// It is at the head of the AQS blocking queue and has successfully acquired the lock (tryAcquire explained above). Set the current to the head and then return the interrupt flag bit to compensate for processing the interrupt operation
setHead(node);
p.next = null; // help GC
return interrupted;
}
// // If the current node is not a header,
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) // block yourself
interrupted = true; // If this condition is met to represent an interrupt awakening, the flag bit needs to be recorded. Then it goes into an endless loop again}}catch (Throwable t) {
cancelAcquire(node);
throwt; }}private final boolean parkAndCheckInterrupt(a) {
LockSupport.park(this); // Park blocks itself. There are only two ways that park can be awakened. First, because unPark is awakened in this way, it should enter the logic to acquire the lock. There is also a case where an interrupt is invoked. In this case, acquire and acquireInterruptibly are divided into two methods, depending on whether the response is received immediately or after the lock is acquired
return Thread.interrupted();
}
Copy the code
2.4 By default, interrupts are not immediately responded. When park is woken up, the thread’s interrupt flag is checked and saved, and park blocks itself before making the call. Until the thread is actually woken up due to unPark, the previous flag bit is retrieved to see if there was an interrupt during the blocking and then an exception is thrown in response to the interrupt.
2.5 In case of immediate response to interrupt, check the interrupt flag when park is woken up. If an exception is thrown directly due to interrupt, and the queue that exits the block does not attempt to acquire the lock. The logic of trying to acquire the lock again if it is not interrupted is the logic of acquireQueue
AcquireInterruptibly 2.4 and 2.5 correspond to Acquire and acquireInterruptibly respectively
The only difference is that during the blocking period if the wake up is caused by interruption, one is to save the flag bit until the lock is acquired in response to the interrupt exception; One is to throw an exception immediately before taking the lock for processing.
unLock
An unLock does not distinguish between a fair lock and an unfair lock. The logic is the same: you subtract state by one, see if state goes to zero (meaning the thread no longer holds the lock) and then wake up the head node in the blocking queue
public void unlock(a) {
sync.release(1);
}
Copy the code
Release is the method in AQS
Release the source code
public final boolean release(int arg) {
if (tryRelease(arg)) { // Try to release the lock. Note: Only threads that already hold the lock can release the lock; Release without holding lock is useless.. . This condition means that the thread no longer holds the lock and needs to wake up another threadNode h = head; Fetch headerif(h ! =null&& h.waitStatus ! =0)
unparkSuccessor(h); // Prepare to wake up the head node in the AQS blocking queue
return true;
}
return false;
}
Copy the code
TryRelease source
protected final boolean tryRelease(int releases) {
int c = getState() - releases; Unlock (Sync release); // Unlock (Sync release); So the same lock must unlock as many times as it reenters
if(Thread.currentThread() ! = getExclusiveOwnerThread())throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
// The current thread no longer holds the lock, so you need to wake up the blocking thread in the blocking queue and set free to indicate that the field no longer holds the lock, and then empty the flag bit of the thread that holds the lock
free = true;
setExclusiveOwnerThread(null);
}
setState(c);// Since only the thread holding the lock can tryRelease, and since it is an exclusive lock, it is thread-safe.
return free; Return true only if there is no reentrant state equal to 0 and wake up other threads. If a thread reenters the UNLOCK twice and only calls it once, no other thread will wake up
}
Copy the code
UnparkSuccessor source
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
node.compareAndSetWaitStatus(ws, 0);
/* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for(Node p = tail; p ! = node && p ! =null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
if(s ! =null)
LockSupport.unpark(s.thread); // Wake up the thread
}
Copy the code
ReentrantLock lock and unLock. But that’s not all. I’m going to continue to summarize multithreading.
There are mistakes in the article, please point them out.