Java concurrency -- waiting and notification between threads

Preface:

We have talked about the principle of concurrent programming. Now we are going to learn about the cooperation between threads. Generally speaking, the current thread needs to wait under certain conditions and does not need to use too many system resources. Under certain conditions, we need to wake it up, allocate certain system resources to it, and let it continue to work. This can save resources better.

I. wait() and notify() of Object

Basic concepts:

When a thread is required to pause because the conditions for executing the target action are not met, it is called wait. When a thread meets the conditions for executing the target action, it is called notify.

Basic template:

synchronized (obj){
            //Protection conditions not established
            while(flag){
                //Pause current thread
                obj.wait();
            }
            //When the protection condition is established, it will jump out while Loop target action
            doAction();
        }

Parsing wait(): the function of Object.wait() is to pause the execution thread. The life cycle of the execution thread is changed to WAITING. Note here that it is infinite WAITING until the thread is notified to wake up by the notify() method. The function of Object.wait(long timeout) is to make the execution thread wake up automatically after a certain period of time without being woken up, that is, to wait for a timeout. Object.wait (long timeout, int nanosecond) is a more precise method to control time, which can be controlled to nanoseconds. It should be noted that wait() can only execute this method when the current thread has a lock and release the lock owned by the current thread, so that the thread can enter the WAITING state, and other threads can try to acquire the current lock. That is to say, it is necessary to apply for lock and release lock.

Parsing notify(): the Object.notify() method wakes up the thread that called wait(), and only wakes up one at most. If there are more than one thread, it may not wake up the thread we want. Object.notifyAll() wakes up all waiting threads. The notify method must be to notify the line program that the lock has been acquired before it can be notified. After notification, the current notification thread needs to release the lock, which is then acquired by the waiting thread. So it involves a step of applying for lock and releasing lock.

There are three problems between wait() and notify():

From the above analysis, we can see that notify() is a non directed wake-up, and notifyAll() is a non biased wake-up. So there are three questions.

Wake up prematurely: suppose that three groups of waiting (w1,w2,w3) and notification (n1,n2,n3) threads are synchronized on object obj. The wake-up conditions of W1 and W2 are the same. Thread n1 updates the conditions and wakes up. The wake-up conditions of w3 are different. N2 and N3 updates the conditions and wakes up. If n1 performs the wake-up, then notify cannot be executed, because two threads need to be woken up, only not can be used. Ifyall(), but when the w3 condition is not met, it will be woken up, and it needs to occupy resources to wait for execution.

Signal loss: this problem is mainly caused by programming problems of programmers, not by internal implementation mechanism. When programming, if you use notify() in the place where you should use notifyAll(), only one thread can wake up, so that other threads that should wake up cannot wake up, which is signal loss. If the waiting thread does not judge whether the protection condition is established before executing the wait() method, it will be notified that the thread has updated the relevant shared variables and executed the notify() method before the waiting thread enters the critical area. However, because the wait() method has not been executed and the judgment of the shared variables has not been set, the wait() method will be executed, causing the thread to be in the Waiting state, a signal is lost.

Deceptive wake-up: waiting threads do not have to have notify()/notifyAll() to wake up. Although the probability is extremely low, the operating system allows this to happen.

Context switching problem: first, wait() will at least cause the thread to apply for and release the internal lock of the corresponding object. When notify()/notifyAll(), you need to hold the corresponding internal lock of the object and release the lock. The problem of context switching occurs, which is actually the change from the RUNNABLE state to the non RUNNABLE state.

Solutions to problems:

Signal loss and deceptive wake-up problems: both can be avoided by using the while loop, as written in the template above.

Context switching problem: using notify() instead of notifyAll() while ensuring the correctness of the program will not lead to early wake-up, so the context switching is reduced. And after using notify, the corresponding internal lock should be released as soon as possible, so that wait() can apply to the lock faster.

Wake up early: use await and signal in java.util.concurrent.locks.Condition.

PS: because wait and notify in Object use native method, which is written in C + +, source code analysis is not done here.

2. await() and signal() in Condition

This method correspondingly changes the above-mentioned problem of non directivity. A queue will be maintained in each condition, so that we can operate more flexibly between threads. Let's take a look at the internal mechanism by analyzing the source code. Condition is an interface. The real implementation is the internal class ConditionObject in AbstractQueuedSynchronizer.

Basic properties:

public class ConditionObject implements Condition, java.io.Serializable {
        private static final long serialVersionUID = 1173984872572414699L;
        /** First node of condition queue. */
        private transient Node firstWaiter;
        /** Last node of condition queue. */
        private transient Node lastWaiter;
}

From the basic properties, it can be seen that the maintenance is a two terminal queue.

Resolution of await() method:

public class ConditionObject implements Condition, java.io.Serializable {
  public final void await() throws InterruptedException {
   // 1. Determine whether the thread is interrupted
    if(Thread.interrupted()){                       
        throw new InterruptedException();
    }
   // 2. Encapsulate a thread into a Node put to Condition Queue inside
    Node node = addConditionWaiter();
   // 3. Release all locks acquired by the current thread (PS: call await Method time, The current thread must have acquired an exclusive lock)              
    int savedState = fullyRelease(node);           
    int interruptMode = 0;
   // 4. Determine whether the current thread is in Sync Queue inside(Here Node from Condtion Queue Move inside to Sync Queue There are two possibilities 
   //(1) other threads call signal to transfer (2) the current thread is interrupted to transfer Node (transfer in checkInterruptWhileWaiting))
while(!isOnSyncQueue(node)){      // 5. The current thread is not in Sync Queue inside, Then proceed block LockSupport.park(this);      // 6. Determine whether the thread wakes up because the thread is interrupted, If interrupted, Will be in checkInterruptWhileWaiting Of transferAfterCancelledWait Transfer nodes;
if((interruptMode = checkInterruptWhileWaiting(node)) != 0){      // It indicates that this is a wake-up by thread interruption., And it's done. node Transfer, Transfer to Sync Queue inside break; } }    // 7. call acquireQueued stay Sync Queue Exclusive lock acquisition in, The return value indicates whether it has been interrupted during the acquisition. if(acquireQueued(node, savedState) && interruptMode != THROW_IE){ interruptMode = REINTERRUPT; }    // 8. adopt "node.nextWaiter != null" Determine whether the thread wakeup is interrupted or not signal.
   //Because the Node representing the thread will exist in both Condition Queue and Sync Queue at the moment when wake-up is interrupted.
if(node.nextWaiter != null){     // 9. Conduct cancelled Cleanup of nodes unlinkCancelledWaiters(); }    // 10. "interruptMode != 0" Represents waking a thread by interrupt if(interruptMode != 0){      // 11. according to interruptMode The type decision for is to throw an exception, I'd better interrupt myself. reportInterruptAfterWait(interruptMode); }   } }

The above source code can see that the queue maintained in Condition is a waiting queue. When the signal() method is called, the current thread node will be transferred from Condition queue to Sync queue queue to compete for lock and wake up.

Source code analysis of signal():

public class ConditionObject implements Condition, java.io.Serializable {
    public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }
    private void doSignal(Node first) {
            do {
                //If the next node of the incoming link list is empty, the tail node is empty.
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                //The next node of the current node is empty
                first.nextWaiter = null;
                //If successful node from condition queue Switch to sync queue,Then exit the loop. If the node is empty, exit the loop. Otherwise, it will find nodes in the queue to wake up.
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }
} 

signal() wakes up any thread in the waiting queue, and signalAll() wakes up all threads in the queue. In this way, we can maintain different threads through different queues to achieve the directional function. The resource loss caused by early wake-up can be eliminated. Note that before using the signal() method, you need to acquire the lock, which is lock(), and then you need to unlock() as soon as possible to avoid the loss of context switching.

Conclusion:

In the object-oriented world, a class often needs to use other classes to complete the calculation together. In the same thread world, multiple threads can complete a task at the same time. Through wake-up and waiting, threads can operate better, so that threads can allocate resources to it when they need to use resources, and they can give resources to other lines when they do not use resources. Process operation. For the Sync queue mentioned in the Condition, please refer to Java concurrency -- combining CountDownLatch source code, Semaphore source code and ReentrantLock source code to see the principle of AQS See how internally maintained queues acquire locks.

Keywords: Java Programming

Added by umbra on Thu, 17 Oct 2019 08:55:53 +0300