Source of this article Principle analysis of AbstractQueuedSynchronizer
Reprint please specify
Abstract queuedsynchronizer, AQS for short, is a common dependency framework for most of Java, such as Lock, Semaphore, CountDownLatch, etc., which implements blocking locks that rely on FIFO waiting queues. Reading its code principle is helpful for us to understand the Java Lock derived class principle and help us develop custom Lock.
Main principles
As shown in the above figure, the elements in the queue are all execution threads. The head of the queue is the execution thread that obtains the exclusive lock. Other threads are sleeping in the queue. The thread that wants to acquire the lock will join the queue from the tail. When the head releases the lock, it wakes up the next thread to acquire the lock, and points the head to the next thread after obtaining the lock successfully. Here is a brief introduction to the basic principles. Let's enter the code explanation together.
Node internal class properties
static final class Node { /** Mark current node shared lock mark */ static final Node SHARED = new Node(); /** Mark current node exclusive lock mark */ static final Node EXCLUSIVE = null; /** Interrupt or timeout exit lock competition */ static final int CANCELLED = 1; /** When the lock is about to be released, you need to wake up the next acquisition thread and point the head node to the next node */ static final int SIGNAL = -1; /** Synchronization queue condition */ static final int CONDITION = -2; //Shared lock static final int PROPAGATE = -3; //Thread waiting states correspond to the above four states respectively volatile int waitStatus; //Front node reference volatile Node prev; //Post node reference volatile Node next; volatile Thread thread; //Next node to wake up Node nextWaiter; final boolean isShared() { return nextWaiter == SHARED; } Node() {} /** Constructor used by addWaiter. */ Node(Node nextWaiter) { this.nextWaiter = nextWaiter; THREAD.set(this, Thread.currentThread()); } /** Constructor used by addConditionWaiter. */ Node(int waitStatus) { WAITSTATUS.set(this, waitStatus); THREAD.set(this, Thread.currentThread()); }
AbstractQueuedSynchronizer internal properties
/** * Queue header */ private transient volatile Node head; /** * Queue tail */ private transient volatile Node tail; /** * Synchronization status. Obtain the lock according to this value. The default thread starts from 0 */ private volatile int state; protected final int getState() { return state; } protected final void setState(int newState) { state = newState; } protected final boolean compareAndSetState(int expect, int update) { return STATE.compareAndSet(this, expect, update); }
When entering ReentrantLock and acquiring the lock, call the AQS method
public void lock() { sync.acquire(1); }
Sync is an internal class of ReentrantLock, which inherits from AbstractQueuedSynchronizer and is used to implement fair locks and unfair locks.
acquire
public final void acquire(int arg) { //Try to get the lock if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //Failed to acquire lock, initialize queue first and then queue selfInterrupt(); //Set current thread interrupt }
The main process obtains the lock first. If it fails, it enters the queue.
acquireQueued
final boolean acquireQueued(final Node node, int arg) { boolean interrupted = false; try { for (;;) { //spin final Node p = node.predecessor(); //Preposition node if (p == head && tryAcquire(arg)) {//The front node is head, and the next attempt to acquire a lock is to acquire a lock setHead(node); //Get success, point the head to node p.next = null; // help GC return interrupted; } if (shouldParkAfterFailedAcquire(p, node)) //Judge whether the predecessor node waitStatus is SIGNAL, if not, the thread will not be suspended interrupted |= parkAndCheckInterrupt(); //Suspend the thread. When the thread is awakened, it will return to thread interrupt state and clear the interrupt } } catch (Throwable t) { cancelAcquire(node); //Wake up the thread and remove the blocking queue if (interrupted) selfInterrupt(); throw t; } }
In the acquirequeueued method, either the lock is acquired successfully to jump out of the loop, or an exception occurs to enter the exception handling logic.
shouldParkAfterFailedAcquire
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) //Thread ready to wake up /* * This node has already set status asking a release * to signal it, so it can safely park. */ return true; if (ws > 0) { //If it is greater than 0, the thread will exit the lock competition do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; //Delete queue node with WS = canceled } else { // Either 0 or promote is changed to /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ pred.compareAndSetWaitStatus(ws, Node.SIGNAL); } return false; }
This method has two functions: comparing waitStatus or setting to move the node forward.
When p=head is equal, it may be head or empty, and the queue has not been initialized successfully, then the lock is acquired once. The other is that the node is the head post node, or the node tries to acquire the lock after re entering. The lock is acquired successfully, and the head node is reset.
cancelAcquire
private void cancelAcquire(Node node) { // Ignore if node doesn't exist if (node == null) return; node.thread = null; Node pred = node.prev; while (pred.waitStatus > 0) //Skip status greater than 0 thread node.prev = pred = pred.prev; Node predNext = pred.next; //Set the current node to cancel led state to exit lock competition node.waitStatus = Node.CANCELLED; //If the node is tail, directly set the front node to tail, if (node == tail && compareAndSetTail(node, pred)) { pred.compareAndSetNext(predNext, null); //take tail.next = null } else { int ws; // To judge whether the front node cannot be head, and then judge whether the state of the node's successor node is legal when the state is SIGNAL // So the conditions are all tenable. Associating the two nodes before and after is equivalent to deleting itself directly. if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && pred.compareAndSetWaitStatus(ws, Node.SIGNAL))) && pred.thread != null) { Node next = node.next; if (next != null && next.waitStatus <= 0) pred.compareAndSetNext(predNext, next);//Anterior posterior integration } else { unparkSuccessor(node); //Wake up post node } node.next = node; // help GC } }
The main task of this method is to find the front node with legal status, set the thread status cancolled, exit the thread lock competition, find the back thread, associate the front and back instructions, and delete yourself from the queue. But it is possible that the front node head needs to wake up the rear node, delete the CANCELLED node and acquire the lock
unparkSuccessor
See how to wake up the thread
private void unparkSuccessor(Node node) { int ws = node.waitStatus; if (ws < 0) //Because to wake up the next thread, the SIGNAL state should be cleared node.compareAndSetWaitStatus(ws, 0); Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node p = tail; p != node && p != null; p = p.prev) //Traverse from back to front to find the closest node if (p.waitStatus <= 0) s = p; } if (s != null) LockSupport.unpark(s.thread); //Wake up thread }
addWaiter
private Node addWaiter(Node mode) { Node node = new Node(mode); for (;;) { Node oldTail = tail; if (oldTail != null) {//Tail already exists, add thread to tail node.setPrevRelaxed(oldTail); if (compareAndSetTail(oldTail, node)) { oldTail.next = node; return node; } } else { initializeSyncQueue(); //Initialize queue head and tail properties } } }
According to the given mode to create a queuing node, mode is divided into two modes Node.EXCLUSIVE Exclusive lock and Node.SHARED Shared lock.
Combined with the analysis of the above three methods, we have some simple understanding of the failure of obtaining lock. The thread uses tryAcquire to acquire the lock. If the acquisition fails, it enters the queuing method. At this time, it first determines that the queue tail exists, and there is already a direct tail insertion method. Otherwise, it initializes the queue tail and head first. It is known that the queue initialization is not initiated by acquiring lock first, but by the thread that fails to compete. This is for performance. Acquire will continue to acquire locks according to the situation that the front node is head, and set an uninterrupted lock competition trend. Then suspend the current thread. When the thread is awakened, the first thing to do is to get the lock successfully and jump out of the acquire loop. Returns the thread interrupt state. Only when the thread interrupt state exists, the thread interrupt is called again.
In some cases, the thread queue picture at the beginning of this article is not the same as the picture display. At the beginning of the queue initialization, the head Node is not the Node to acquire the lock. The head Node creates the Node object randomly, which is the same as the tail. When a thread from tail.next Association, enter the queue, and also introduce the relationship with the head. As long as the Node closest to the head obtains the lock, it will point the head to itself before going to the direction of the opening queue picture.
Analyze tryAcquire
tryAcquire is the only implementation of acquiring lock, which is mainly implemented by subclass. The main reason is that AQS supports exclusive lock and shared lock, and whether it supports reentry. It is more suitable for subclass to implement the lock acquisition logic. Enter ReentranLock to learn how to implement tryAcquire for fair lock and unfair lock. Analyze the two situations together.
Unfair lock
static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } } abstract static class Sync extends AbstractQueuedSynchronizer { @ReservedStackAccess final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) {//state competes for lock for the first time if (compareAndSetState(0, acquires)) { //If the exchange comparison state is successful, the lock is obtained successfully setExclusiveOwnerThread(current);//Set property variable return true; } } else if (current == getExclusiveOwnerThread()) { //Lock reentry, only for state + = acquire int nextc = c + acquires; if (nextc < 0) // int out of bounds throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } }
It is very simple. CAS (exchange comparison) is used to set the successful thread first. Even if it is successful, it can be used to determine whether the thread acquiring lock is the thread occupying lock and support re-entry lock.
Fair lock
static final class FairSync extends Sync { @ReservedStackAccess protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } }
The main difference is that there is an additional judgment of hasQueuedPredecessors, which is under method analysis
public final boolean hasQueuedPredecessors() { Node h, s; if ((h = head) != null) {//Queue already initialized //If it is greater than 0, it is canceled. s will be removed from the queue. The next node is not allowed. Get the next node if ((s = h.next) == null || s.waitStatus > 0) { s = null; // traverse in case of concurrent cancellation for (Node p = tail; p != h && p != null; p = p.prev) { //Traversal from tail chain to front if (p.waitStatus <= 0) //waitStatus status OK s = p; } } if (s != null && s.thread != Thread.currentThread()) //The thread to acquire the lock as long as it is not the next thread to acquire the lock return true; } return false; }
First, judge whether the head node is initialized. If not, return false directly. There is already a successor node that directly obtains the head node. Only when the node thread is judged to be unequal to the current thread, it returns true. Why is that? Think about when the lock starts to be released, wake up the next node to acquire the lock. At this time, a thread will also acquire the lock. It is possible to seize the queued node in the queue to acquire the lock, which is equivalent to queue jumping, which is "unfair".
The main difference between public and unfair is to judge whether there is a queuing thread in the queue before acquiring the lock. If there is any, the principle of directly acquiring the lock fails, entering the queue, and forcibly ensuring that the queue with the longest queue obtains the lock first. Fair locks consume a little more performance than unfair locks, but the impact is not great. It is also a good choice to use fair locks in normal development. After all, everyone chooses to pursue fairness 😝.
Release lock
ReentranLock.unlock() code
public void unlock() { sync.release(1); }
release
public final boolean release(int arg) { if (tryRelease(arg)) { //This needs to be implemented by subclasses Node h = head; if (h != null &&stat h.waitStatus != 0) //Status cannot be 0 unparkSuccessor(h); //Wake up next queue thread return true; } return false; }
h.waitStatus = 0 is the status or the default status. We know that the head status is determined by shouldParkAfterFailedAcquire Modified. At this time, the thread is still spinning to acquire the lock, so it does not need to wake up.
tryRelease
ReentranLock's tryRelease is the implementation of fair lock and unfair lock.
protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) //Release lock thread must be owner thread, otherwise exception will be thrown directly throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { //State must be equal to 0 to release the lock. It corresponds to the lock re entering state calculation free = true; setExclusiveOwnerThread(null); } setState(c); return free; }
Every time tryRelease() is called, as long as it is not an illegal thread, it can let state - releases, and it will not wake up the thread. As long as state=0, the lock is really released, and the occupation thread is set to null. The waiting thread in the wake-up queue.
A ReentranLock get lock release lock process is finished, but there are many methods in AbstractQueuedSynchronizer. In order to analyze this type, analyze other functions.
lockInterruptibly
When the lock is acquired or queued, the thread can be forced to exit the lock competition. This is the only function of ReentrantLock. The synchronized keyword does not support getting lock interrupt.
ReentrantLock code
public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); }
acquireInterruptibly
Take a look at acquireinterruptible
public final void acquireInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) //It has been interrupted, and the interrupt status has been cleaned up throw new InterruptedException(); if (!tryAcquire(arg)) //Failed to acquire lock doAcquireInterruptibly(arg); //Failed to list pending }
Let's see how the queued thread supports interrupts
doAcquireInterruptibly
private void doAcquireInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.EXCLUSIVE); //Join queue current node try { for (;;) { //spin final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC return; } // First check the state of the front node and remove the invalid thread if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) //Suspend thread returns thread interrupt status throw new InterruptedException(); } } catch (Throwable t) { cancelAcquire(node); throw t; } }
The lock acquisition process is basically the same as the lock method. Before acquiring the lock, the thread interrupt will be judged and it will be processed. In the queued thread, if the thread has been suspended, the interrupt cannot be handled. As long as the thread is awakened, an exception is thrown directly to exit the lock competition.
Acquire lock timeout
ReentrantLock has a method that can be set to acquire the lock within a specified time to avoid the thread being blocked all the time while waiting for the lock to be acquired. By setting the timeout, the caller can handle the failure of lock acquisition. Direct access to code explanation
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout)); }
How to realize AQS
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); return tryAcquire(arg) || //Get failed enter the following method doAcquireNanos(arg, nanosTimeout); }
doAcquireNanos
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (nanosTimeout <= 0L) return false; final long deadline = System.nanoTime() + nanosTimeout; //Time out stamp final Node node = addWaiter(Node.EXCLUSIVE); //Join the team try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC return true; } nanosTimeout = deadline - System.nanoTime(); if (nanosTimeout <= 0L) { //Has timed out cancelAcquire(node); // Exit queue return false; } if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD) //Threads larger than 1000 ns need to be suspended. Otherwise, the time is too short, and it will end before the suspension LockSupport.parkNanos(this, nanosTimeout); //Suspend thread, the specified time will be longer than wake-up if (Thread.interrupted()) throw new InterruptedException(); } } catch (Throwable t) { cancelAcquire(node); throw t; } }
According to Baidu's description of nanoseconds, it takes about 2 to 4 nanoseconds for a computer to execute an instruction (such as adding two numbers), so it doesn't make sense if the time interval is too small. This method has more timeout judgment and timeout wake-up threads than the normal method, and the others are the same.
ConditionObject
ConditionObject is an internal class of AbstractQueuedSynchronizer, which implements the main functions of the Condition interface to block threads and wake-up blocking. It is similar to wait, notify and notifyAll. It is generally used to block and wake up threads of producers and consumers of synchronous queues. First, analyze the meaning of each method of the Condition interface, and then analyze the method implementation.
Condition
public interface Condition { // Causes the current thread to wait until it receives a signal or an interrupt void await() throws InterruptedException; //Causes the current thread to wait until it receives a signal void awaitUninterruptibly(); // Causes the current thread to wait until a signal is received or a specified time is exceeded or an interrupt is issued long awaitNanos(long nanosTimeout) throws InterruptedException; // ditto boolean await(long time, TimeUnit unit) throws InterruptedException; //The current thread waits until it receives a message, interrupts, or exceeds a specified time boolean awaitUntil(Date deadline) throws InterruptedException; // Wake up a single waiting thread void signal(); //Wake up all waiting threads void signalAll();
Before parsing the implementation class, you need to know the internal properties of ConditionObject
public class ConditionObject implements Condition, java.io.Serializable { /** First node of condition queue. */ private transient Node firstWaiter; /** Last node of condition queue. */ private transient Node lastWaiter; public ConditionObject() { }
There are only two internal attributes, the head node and the tail node. The implementation method is analyzed below.
await
public final void await() throws InterruptedException { if (Thread.interrupted()) //Returns the thread interrupt status and clears the interrupt signal throw new InterruptedException(); Node node = addConditionWaiter(); //Thread enters the waiting queue int savedState = fullyRelease(node); //Perform the lock release function and return the state value int interruptMode = 0; // //Whether the node is in the queue while (!isOnSyncQueue(node)) { //Do not suspend thread in queue LockSupport.park(this); //When the thread is waked up, clean up the interrupt state and rejoin the waiting queue //When the thread is awakened, judge whether there is an interrupt signal and return it to interruptMode if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } //Spin to acquire lock in the team if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; if (node.nextWaiter != null) // clean up if cancelled unlinkCancelledWaiters(); //Clear thread state is not CONDITION if (interruptMode != 0) reportInterruptAfterWait(interruptMode); }
- First, judge whether the thread is interrupted, and then throw an exception directly.
- Add Node to the one-way linked list maintained by ConditionObject. This list is mainly used for queue wake-up and does not conflict with the queue for obtaining lock.
- Release the lock. I didn't understand it at first. This should be understood in combination with the synchronization queue. When a thread is suspended, it must release the lock, or it will become a deadlock. The producer can't get the lock, insert the data, and wake up the consumer.
- When loop processing, as long as the node has joined the thread queue to participate in the lock competition, it will exit the loop or suspend the current thread first. When the thread is awakened, judge whether the thread is awakened due to interruption. If so, jump out of the while loop directly.
- Now that you have joined the queue, you can get the lock.
- At this time, the node node has acquired the lock waitStatus, which has changed. It is necessary to clear the illegal nodes in the one-way linked list, including itself.
- When interruptMode is not equal to 0, it means that there is an interrupt that needs to be handled by the caller himself.
addConditionWaiter
private Node addConditionWaiter() { if (!isHeldExclusively()) //Not the current lock owner throw new IllegalMonitorStateException(); Node t = lastWaiter; // If the lastWaiter is not in the CONDITION state, remove the queue, and the CONDITION is the exclusive state of the synchronization queue if (t != null && t.waitStatus != Node.CONDITION) { unlinkCancelledWaiters(); //Remove nextWaiter queue with status not equal to CONDITION t = lastWaiter; } Node node = new Node(Node.CONDITION); //Create a node and set waitStatus to CONDITION if (t == null) //The header node has not been initialized firstWaiter = node; else t.nextWaiter = node; //node is either the head or the tail lastWaiter = node; return node; }
In conditionioinobject, a one-way condition list connected by nextWaiter will be maintained. The main features of this list are as follows Node.waitStatus Must be Node.CONDITION , will put the head node and tail node of the list into the lastWaiter and firstWaiter nodes.
unlinkCancelledWaiters
private void unlinkCancelledWaiters() { Node t = firstWaiter; Node trail = null; while (t != null) { //Traverse from the beginning Node next = t.nextWaiter; if (t.waitStatus != Node.CONDITION) { t.nextWaiter = null; //Cut off the association between t and subsequent nodes, which is easy to delete below if (trail == null) //Delete the head node. The next node is the head node firstWaiter = next; else trail.nextWaiter = next; if (next == null) lastWaiter = trail; } else trail = t; t = next; } }
Traverse the entire condition list, and Node.waitStatus ! Node.CONDITION Delete.
isOnSyncQueue
final boolean isOnSyncQueue(Node node) { if (node.waitStatus == Node.CONDITION || node.prev == null) //node has just released the lock and has not entered the queue once return false; if (node.next != null) //The front node is not empty, it must be in the queue return true; return findNodeFromTail(node); //Find node from tail and return true }
checkInterruptWhileWaiting
private int checkInterruptWhileWaiting(Node node) { return Thread.interrupted() ? (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 0; }
Check for thread breaks and clean it up. Return 0 without interruption, and transferaftercanceledwait will not be executed. When interrupted, the transferaftercanceledwait is executed and an interrupt structure is returned.
- REINTERRUPT interrupt exit while waiting for exit
- THROW_IE throws an InterruptedException exception to exit
transferAfterCancelledWait
final boolean transferAfterCancelledWait(Node node) { if (node.compareAndSetWaitStatus(Node.CONDITION, 0)) { //Waitstatus condition = > 0 set successfully enq(node); //Join queue return true; } while (!isOnSyncQueue(node)) //node is not in the queue Thread.yield(); //Thread gives up execution right and waits for node to enter the queue and then jumps out of spin return false; }
According to the previous code for obtaining lock, waitStatus is the default value of 0. Only when aitstatus condition = > 0 is set successfully can we join the queue and participate in lock competition. If the setting fails, it will spin to determine whether the node is in the queue, and it will exit only when it is in the queue.
reportInterruptAfterWait
private void reportInterruptAfterWait(int interruptMode) throws InterruptedException { if (interruptMode == THROW_IE) // Throw the interrupt exception again and hand it to the caller for handling throw new InterruptedException();xin else if (interruptMode == REINTERRUPT) //Interrupt exit on exit selfInterrupt(); //Call thread interrupt }
According to different interruptions, different treatments are made.
signal
public final void signal() { if (!isHeldExclusively()) //Must be exclusive lock thread execution throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null) //The header node is not empty, indicating that the condition queue exists and can be waked up doSignal(first); //Wake up thread }
See how the wake-up thread is implemented
doSignal
private void doSignal(Node first) { do { if ( (firstWaiter = first.nextWaiter) == null) //The next node is empty, and the list is finished lastWaiter = null; first.nextWaiter = null; //This method is the main wake-up logic } while (!transferForSignal(first) && (first = firstWaiter) != null); }
transferForSignal
final boolean transferForSignal(Node node) { if (!node.compareAndSetWaitStatus(Node.CONDITION, 0)) //Join queue waitStatus=0 return false; //Join queue competition lock // p is the node front node Node p = enq(node); int ws = p.waitStatus; // If ws is greater than 0 P, it will exit the queue and wake up the post node // p failed to set the state. The p node can no longer exist. At this time, the thread should be awakened to acquire the lock. if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL)) LockSupport.unpark(node.thread); //Wake up node's thread return true; //Wake up success returns true }
Here, the condition of wake-up thread is whether the front node is available, as long as the front node is ready to exit the queue or has been deleted. The node is awakened to acquire the lock.
I'm not going to write the rest of the methods. There's too much content. I'm interested in learning by myself. The content is almost repetitive.
summary
This paper analyzes the internal source code principle of AbstractQueuedSynchronizer through the principle of obtaining lock releasing lock, and also analyzes the difference between fair lock and unfair lock. Explain the locks of some derivative functions, such as timeout lock and interrupt lock. It completely analyzes AbstractQueuedSynchronizer as the framework support of Java ReentrantLock, and simply analyzes ConditionObject's thread coordination signal and await methods. Most of the content of this article is my own thinking. If there are any mistakes, please come out and discuss with us.