Detailed explanation of Condition's await and signal waiting notification mechanism

1. Introduction to Conditions

Any java object is naturally inherited from the Object class. To achieve communication between threads, several methods of Object are often applied, such as wait(), wait(long timeout), wait(long timeout, int nanos) and notify() ,notifyAll() several methods to implement the waiting/notification mechanism, similarly, there will still be the same method to implement the waiting/notification mechanism under the java Lock system. On the whole, wait and notify/notify of Object cooperate with the object monitor to complete the waiting/notification mechanism between threads, while Condition and Lock cooperate to complete the waiting notification mechanism. The former is at the bottom level of java, and the latter is at the language level. It has higher controllability and scalability . In addition to the difference in the way of use, the two still have many differences in functional characteristics :

  1. Condition can support not responding to interrupts, but it does not support it by using Object;
  1. Condition can support multiple waiting queues (new multiple Condition objects), while the Object method can only support one;
  1. Condition can support the setting of timeout, but Object does not support it

Referring to Object's wait and notify/notifyAll methods, Condition also provides the same method:

The wait method for Object

  1. void await() throws InterruptedException: The current thread enters the waiting state. If other threads call the signal or signalAll method of the condition and the current thread obtains the Lock and returns from the await method, if it is interrupted in the waiting state, an interrupted exception will be thrown;
  1. long awaitNanos(long nanosTimeout): The current thread enters the waiting state until it is notified, interrupted or timed out ;
  1. boolean await(long time, TimeUnit unit) throws InterruptedException: Same as the second one, supports custom time unit
  1. boolean awaitUntil(Date deadline) throws InterruptedException: The current thread enters the waiting state until it is notified, interrupted or a certain time

notify/notifyAll method for Object

  1. void signal(): wake up a thread waiting on the condition, transfer the thread from the waiting queue to the synchronization queue , and return from the waiting method if the lock can be competed in the synchronization queue.
  1. void signalAll(): The difference from 1 is that it can wake up all threads waiting on the condition

2. Analysis of the principle of Condition implementation

2.1 Waiting queue

If you want to have an in-depth grasp of condition, you should know its implementation principle. Now let's take a look at the source code of condiiton. Creating a condition object is passed lock.newCondition(), and this method will actually create a new ConditionObject object. This class is an internal class of AQS ( the article on the implementation principle of AQS ). If you are interested, you can take a look. As we said before, condition is to be used in conjunction with lock, that is, condition and Lock are bound together, and the implementation principle of lock depends on AQS. Naturally, it is understandable that ConditionObject is an internal class of AQS. We know that in the implementation of the lock mechanism, AQS maintains a synchronization queue internally. If it is an exclusive lock, the tails of all threads that fail to acquire the lock are inserted into the synchronization queue. Similarly, the same method is used inside the condition. Internal maintenance A waiting queue is established , and all threads that call the condition.await method will be added to the waiting queue, and the thread state will be converted to the waiting state. Also notice that there are two member variables in ConditionObject:

/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;

In this way, we can see that ConditionObject manages the waiting queue by holding the head and tail pointers of the waiting queue. The main thing to note is that the Node class reuses the Node class in AQS. For its node status and related attributes, you can read the article on the implementation principle of AQS . If you read this article carefully, it is easy to understand the condition and the lock system. There will also be a qualitative improvement in implementation. The Node class has such a property:

//后继节点
Node nextWaiter;

To further explain, the waiting queue is a one-way queue , and when I said AQS before, I knew that the synchronization queue was a two-way queue. Next, we use a demo and go in through debug to see if it meets our guess:

public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
        Thread thread = new Thread(() -> {
            lock.lock();
            try {
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        });
        thread.start();
    }
}

This code doesn't make any sense, it even stinks, it's just to illustrate what we just thought. Created 10 new threads. If no thread acquires the lock first, then call the condition.await method to release the lock and add the current thread to the waiting queue. Through debug control, when you reach the 10th thread, you can check the head node in the waiting firstWaiterqueue , the scene diagram in debug mode is as follows:

From this figure, we can clearly see the following points: 1. After calling the condition.await method, the threads are inserted into the waiting queue in sequence. The thread references in the queue are Thread-0, Thread-1, and Thread- 2...Thread-8; 2. The waiting queue is a one-way queue. Through our conjecture and then experimental verification, we can draw a schematic diagram of the waiting queue as shown in the figure below:

At the same time, there is another point to note: we can call the lock.newCondition() method multiple times to create multiple condition objects, that is, a lock can hold multiple waiting queues. The previous method of using Object actually means that there can only be one synchronization queue and one waiting queue on the object monitor, while the Lock in the concurrent package has one synchronization queue and multiple waiting queues . The schematic diagram is as follows:

As shown in the figure, ConditionObject is an internal class of AQS, so each ConditionObject can access the methods provided by AQS, which means that each Condition has a reference to its own synchronizer.

2.2 await implementation principle

When the condition.await() method is called, the thread currently acquiring the lock will enter the waiting queue. If the thread can return from the await() method, it must be that the thread has acquired the lock associated with the condition . Next, we still look at it from the perspective of the source code. Only when we are familiar with the logic of the source code can our understanding be the deepest. The source code of await() method is:

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
	// 1. 将当前线程包装成Node,尾插入到等待队列中
    Node node = addConditionWaiter();
	// 2. 释放当前线程所占用的lock,在释放的过程中会唤醒同步队列中的下一个节点
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
		// 3. 当前线程进入到等待状态
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
	// 4. 自旋等待获取到同步状态(即获取到lock)
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
	// 5. 处理被中断的情况
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

Please see the comments for the main logic of the code . We all know that when the current thread calls the condition.await() method, the current thread will release the lock and then join the waiting queue until it is signal/signalAll, and the current thread will be removed from the waiting queue. Move to the synchronization queue, and will not return from the await method until the lock is obtained, or interrupt processing will be done if it is interrupted while waiting . Then we have several questions about this implementation process: 1. How to add the current thread to the waiting queue? 2. The process of releasing the lock? 3. How can I exit from the await method? The logic of this code is to tell us the answers to these three questions. Please refer to the comments for details . In step 1, call addConditionWaiter to add the current thread to the waiting queue. The source code of this method is:

private Node addConditionWaiter() {
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
	//将当前线程包装成Node
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
		//尾插入
        t.nextWaiter = node;
	//更新lastWaiter
    lastWaiter = node;
    return node;
}

This code is easy to understand, wrap the current node into a Node, if the firstWaiter of the waiting queue is null (the waiting queue is an empty queue), point the firstWaiter to the current Node, otherwise, just update the lastWaiter (tail node) . It is to insert the Node encapsulated by the current thread into the waiting queue through tail insertion . At the same time, it can be seen that the waiting queue is a chained queue without a leading node . When we learned AQS before, we knew that the synchronization queue was a leading node. The chained queue , which is a difference between the two. After inserting the current node into the waiting queue, the current thread will release the lock, which is implemented by the fullyRelease method. The source code of fullyRelease is:

final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        int savedState = getState();
        if (release(savedState)) {
			//成功释放同步状态
            failed = false;
            return savedState;
        } else {
			//不成功释放同步状态抛出异常
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}

This code is easy to understand. Call the template method release method of AQS to release the synchronization state of AQS and wake up the thread referenced by the successor node of the head node in the synchronization queue. If the release is successful, it will return normally, and if it fails, it will throw abnormal. So far, these two pieces of code have solved the answers to the first two questions, and the third question remains, how to exit from the await method? Now look back and look at the await method with such a logic:

while (!isOnSyncQueue(node)) {
	// 3. 当前线程进入到等待状态
    LockSupport.park(this);
    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
        break;
}

Obviously, when the thread calls the condition.await() method for the first time, it will enter the while() loop, and then make the current thread enter the waiting state through the LockSupport.park(this) method, then if you want to exit the await method The first prerequisite is naturally to exit the while loop first, and there are only two exits left: 1. The logic goes to break to exit the while loop; 2. The logic judgment in the while loop is false . Looking at the code again, the condition for the first case is that the code will go to break and exit after the currently waiting thread is interrupted. The second case is that the current node is moved to the synchronization queue (that is, the signal or signalAll method of the condition called by another thread ), the while loop ends after the logic judgment in the while is false. To sum up, after the current thread is interrupted or the current node is moved to the synchronization queue by calling the condition.signal/condition.signalAll method , this is the prerequisite for the current thread to exit the await method. It will be called after exiting the while loop acquireQueued(node, savedState). This method is mentioned in the introduction of the underlying implementation of AQS. If you are interested, you can read this article . The function of this method is that the thread continuously tries to obtain the synchronization state during the spin process. Until success (the thread acquires the lock) . This also shows that exiting the await method must be a lock that has obtained a condition reference (association) . So far, we have completely found the answers to the first three questions by reading the source code, and we have also deepened our understanding of the await method. The schematic diagram of the await method is as follows:

As shown in the figure, the thread calling the condition.await method must have acquired the lock, that is, the current thread is the head node in the synchronization queue. After calling this method, the Node tail encapsulated by the current thread will be inserted into the waiting queue.

Timeout mechanism support

condition also additionally supports a timeout mechanism, and users can call methods awaitNanos and awaitUtil. The implementation principles of these two methods are basically the same as the tryAcquire method in AQS. For tryAcquire, you can read Section 3.4 of this article carefully .

Support for unresponsive interrupts

If you want to not respond to interrupts, you can call the condition.awaitUninterruptibly() method. The source code of this method is:

public final void awaitUninterruptibly() {
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    boolean interrupted = false;
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if (Thread.interrupted())
            interrupted = true;
    }
    if (acquireQueued(node, savedState) || interrupted)
        selfInterrupt();
}

This method is basically the same as the await method above, except that the processing of interrupts is reduced, and the interrupted exception thrown by the reportInterruptAfterWait method is omitted.

2.3 signal/signalAll implementation principle

Calling the signal or signalAll method of condition can move the node with the longest waiting time in the waiting queue to the synchronization queue , so that the node can have the opportunity to obtain the lock. According to the waiting queue is first-in-first-out (FIFO), so the head node of the waiting queue must be the node with the longest waiting time, that is, each time the signal method of condition is called, the head node is moved to the synchronization queue. Let's see if this conjecture is correct by looking at the source code. The source code of the signal method is:

public final void signal() {
    //1. 先检测当前线程是否已经获取lock
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //2. 获取等待队列中第一个节点,之后的操作都是针对这个节点
	Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

The signal method will first check whether the current thread has acquired the lock. If the lock has not been acquired, an exception will be thrown directly. If it is acquired, then the node referenced by the head pointer of the waiting queue will be obtained. The doSignal method of subsequent operations is also based on this node. Let's take a look at what the doSignal method does. The source code of the doSignal method is:

private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
		//1. 将头结点从等待队列中移除
        first.nextWaiter = null;
		//2. while中transferForSignal方法对头结点做真正的处理
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

Please refer to the comments for the specific logic. The logic for actually processing the head node is placed in transferForSignal . The source code of this method is:

final boolean transferForSignal(Node node) {
    /*
     * If cannot change waitStatus, the node has been cancelled.
     */
	//1. 更新状态为0
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    /*
     * Splice onto queue and try to set waitStatus of predecessor to
     * indicate that thread is (probably) waiting. If cancelled or
     * attempt to set waitStatus fails, wake up to resync (in which
     * case the waitStatus can be transiently and harmlessly wrong).
     */
	//2.将该节点移入到同步队列中去
    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

Please see the comments for the key logic. This code mainly does two things: 1. Change the state of the head node to CONDITION; 2. Call the enq method to insert the end of the node into the synchronization queue. For the enq method, please refer to AQS The underlying implementation of this article. Now we can draw a conclusion: the prerequisite for calling the signal of condition is that the current thread has acquired the lock, this method will make the head node in the waiting queue, that is, the node with the longest waiting time, move into the synchronization queue, and move into the synchronization queue Then there is a chance to wake up the waiting thread, that is, to return from the LockSupport.park(this) method in the await method, so that the thread calling the await method has a chance to exit successfully . The schematic diagram of signal execution is as follows:

signalAll

The difference between sigllAll and sigal methods is reflected in the doSignalAll method. We already know that the doSignal method will only operate on the head node of the waiting queue, and the source code of doSignalAll is:

private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    } while (first != null);
}

This method is just waiting for each node in the queue to be moved into the synchronization queue, that is, to "notify" each thread that is currently calling the condition.await() method.

3. Combination of await and signal/signalAll

The beginning of the article mentions the waiting/notification mechanism, which can be realized by using the await and signal/signalAll methods provided by condition, and this mechanism can solve the most classic problem is "producer and consumer problem", about "production "Consumer Issues" will be explained in a separate article, which is also a high-frequency test point for interviews. The await and signal and signalAll methods are like a switch that controls thread A (waiting side) and thread B (notifying side). The relationship between them can be expressed more closely with the following diagram:

As shown in the figure, the thread awaitThread first acquires the lock successfully through the lock.lock() method and then calls the condition.await method to enter the waiting queue, while another thread signalThread successfully acquires the lock through the lock.lock() method and then calls condition.signal or signalAll method, so that the thread awaitThread can have the opportunity to move into the synchronization queue. When other threads release the lock, the thread awaitThread can have the opportunity to acquire the lock, so that the thread awaitThread can exit from the await method to perform subsequent operations. If the awaitThread fails to acquire the lock, it will directly enter the synchronization queue .

3. An example

Let's use a very simple example to talk about the usage of condition:

public class AwaitSignal {
    private static ReentrantLock lock = new ReentrantLock();
    private static Condition condition = lock.newCondition();
    private static volatile boolean flag = false;

    public static void main(String[] args) {
        Thread waiter = new Thread(new waiter());
        waiter.start();
        Thread signaler = new Thread(new signaler());
        signaler.start();
    }

    static class waiter implements Runnable {

        @Override
        public void run() {
            lock.lock();
            try {
                while (!flag) {
                    System.out.println(Thread.currentThread().getName() + "当前条件不满足等待");
                    try {
                        condition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "接收到通知条件满足");
            } finally {
                lock.unlock();
            }
        }
    }

    static class signaler implements Runnable {

        @Override
        public void run() {
            lock.lock();
            try {
                flag = true;
                condition.signalAll();
            } finally {
                lock.unlock();
            }
        }
    }
}

The output is:

Thread-0当前条件不满足等待
Thread-0接收到通知,条件满足

Two threads waiter and signaler are opened. When the waiter thread starts to execute, because the condition is not satisfied, the condition.await method is executed to make the thread enter the waiting state and release the lock at the same time. After the signaler thread acquires the lock, the condition is changed and all waiting threads are notified. Then release the lock. At this time, the waiter thread acquires the lock, and because the signaler thread has changed the condition, the condition is satisfied relative to the waiter, and continues to execute.

references

"The Art of Java Concurrent Programming"

Guess you like

Origin blog.csdn.net/weixin_60257072/article/details/129584302