Blocking and waking up, waiting for the stage of the queue

1, Foreword

In the previous article, we introduced the lock method and unlock method in the AQS source code. These two methods are mainly used to solve the problem of mutual exclusion in concurrency. In this article, we mainly introduce the await method, signal method and signalAll method used to solve the problem of thread synchronization in AQS. These methods mainly correspond to the wait method, notify method and notifAll method in synchronized.

2, Practical application of await() and signal()/signalAll()

2.1 practical application of await() and signal()/signalAll()

We implement a blocked queue.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyBlockedQueue<T> {
    final Lock lock = new ReentrantLock();  // With Lock, producers and consumers compete for the same Lock. With synchronized, producers and consumers compete for the same Lock object
    // Condition variable: queue dissatisfaction
    final Condition notFull = lock.newCondition();
    // Condition variable: queue is not empty
    final Condition notEmpty = lock.newCondition();
    private volatile List<T> list = new ArrayList<>();

    // Join the team
    void enq(T x) {
        lock.lock();
        try {
            while (list.size() == 10) {
                // Waiting queue dissatisfied
                try {
                    notFull.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // Omit queue operation
            list.add(x);
            // After joining the team, inform to leave the team
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    // Out of the team
    void deq() {
        lock.lock();
        try {
            while (list.isEmpty()) {
                // Waiting queue is not empty
                try {
                    notEmpty.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            list.remove(0);
            // After leaving the team, notify to join the team
            notFull.signal();
        } finally {
            lock.unlock();
        }
    }

    public List<T> getList() {
        return list;
    }


    public static void main(String[] args) throws InterruptedException {
        MyBlockedQueue<Integer> myBlockedQueue = new MyBlockedQueue<>();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    myBlockedQueue.enq(i);
                }
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    myBlockedQueue.deq();
                }
            }
        });
        thread1.start();
        thread2.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Arrays.toString(myBlockedQueue.getList().toArray()));
    }
}

The operation results are as follows (the last 10 bits are output):

[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

We can see that condition is used in multithreading, which is similar to the communication before thread:
(1) When a condition is met, the operation in a thread is executed;
(2) When a condition is not met, suspend the current thread. When the condition is met, other threads wake up the current thread.

When writing the producer consumer model, a common error prone point must be remembered. If Lock is used, the producer and consumer compete for the same Lock. If synchronized is used, the producer and consumer compete for the same Lock object.

2.2 ConditionObject class and Node class

2.2.1 ConditionObject class

Properties of the ConditionObject class

public class ConditionObject implements Condition, java.io.Serializable {
    private static final long serialVersionUID = 1173984872572414699L;   // Implement the Serializable interface to explicitly specify the serialization field
    private transient Node firstWaiter;   // As a Node type, it points to the first Node in the waiting queue
    private transient Node lastWaiter;    // As a Node type, it points to the last Node in the waiting queue
    private static final int REINTERRUPT =  1;  // reinterrupt is set as the final variable, and the name is easy to understand
    private static final int THROW_IE    = -1;  // throw InterruptedException
}

Method of ConditionObject class

It is worth noting that synchronized + wait()+notify() is equivalent to lock + await() + signal(). Therefore, condition is equivalent to wait()/notify(). Condition is wait()+notify() implemented in JUC package.

Another thing to note is that the lock pool and the waiting pool are two independent things. The wait() blocking into the waiting pool is not the end of the synchronization code block. The current thread releases the lock, but the synchronization code block does not end. Once awakened, it is still necessary to start from the blocked place, execute the unfinished synchronization code block, and know that the synchronization code block ends, Before releasing the synchronization lock and entering the lock pool.

2.2.2 Node class

For Node nodes, the attributes include seven (with emphasis on the first five)

volatile int waitStatus; //Current node waiting state
volatile Node prev; //Previous node
volatile Node next; //Next node
volatile Thread thread; //Value in node
Node nextWaiter; //Next waiting node
static final Node SHARED = new Node();  //Indicates whether the node is shared or exclusive. The default initial is shared
static final Node EXCLUSIVE = null;

(1) The attributes (locking and unlocking) used in the synchronization queue include: next, prev, thread and waitStatus. Therefore, the synchronization queue is a two-way acyclic linked list. The class variables involved, head and tail in the AbstractQueuedSynchronizer class, point to the head node and tail node in the synchronization queue respectively.

(2) The attributes (blocking and wake-up) used in the waiting queue include nextWaiter, thread and waitStatus. Therefore, the waiting queue is a one-way acyclic linked list. The class variables involved, firstWaiter and lastWaiter in the ConditionObject class, point to the head node and tail node in the waiting queue respectively.

(3) AQS queues are work queues, synchronous queues and acyclic two-way queues: when head tail is used, AQS queues are established. A single thread does not use head tail, so AQS queues are not established;

(4) The waiting queue is an acyclic one-way queue: when the firstwait lastwait is used, the waiting queue is established.

(5) lock() and unlock() are the operation synchronization queue: lock() encapsulates the thread into the node (at this time, the attributes used by the node are thread, nextWaiter and waitStatus) and puts them into the synchronization queue / AQS queue. unlock() takes the node storing the thread out of the synchronization queue, indicating that the thread has completed its work.

(6) await() and signal() are the operation waiting queue: await() encapsulates the thread into the node (at this time, the attribute used by the node is thread prev next waitStatus), puts it into the waiting queue, and signal() takes out the elements from the waiting queue.

Question: Why are the head and tail responsible for synchronizing the queue in the AbstractQueuedSynchronizer class, but the firstWaiter and lastWaiter responsible for waiting the queue in the ConditionObject class?
answer:
(1) For thread synchronization mutual exclusion, it is directly implemented through ReentrantLock class objects lock.lock(), lock.unlock(), and ReentrantLock class objects call AQS class to unlock locks. Therefore, the head and tail responsible for synchronization queue are in AbstractQueuedSynchronizer class;
(2) For thread blocking and wake-up, a correspondence is obtained through the ReentrantLock class object lock.newCondition(), the condition reference points to this object, and then the condition. Await() and condition. Signal() are implemented. Therefore, the firstWaiter and lastWaiter responsible for waiting for the queue are in the ConditionObject class.

3, await() source code

3.1 Condition.await() execution chart

Let's take a look at the source code of await, as shown in the figure below:

For the explanation of the above figure:
The first method is inserted into the waiting queue, the second method releases the synchronization lock, and the third method blocks the current thread. The three methods are a whole and cannot be separated. For example, the second method precedes the third method, which means that the synchronization lock is released first and then the thread is suspended. The purpose is to avoid being suspended when the current thread does not release the lock, resulting in other threads failing to obtain the lock or causing deadlock.

Overall process details:

In the first step, if a thread calls the await method, it will add the waiting thread to the AQS waiting queue through CAS and tail interpolation (the CAS and tail interpolation method means that when CAS ensures thread safety, the tail interpolation method is used to put the thread into a Node node and insert it into the AQS waiting queue in three steps), Corresponding code Node = addconditionwaiter();

The second step is to unlock the current thread (the purpose of unlocking the current thread is to prevent the thread from being suspended when it does not release the lock, resulting in other threads not obtaining the lock or causing deadlock). The corresponding code is int savedState = fullyRelease(node);

Step 3: park the current thread (after Park, this thread can only passively wait for other threads to call the signal method to unpark the current thread), corresponding to the code LockSupport.park(this);

while (!isOnSyncQueue(node)) {    // If the node returned by addConditionWaiter() is not in the synchronization queue, it will be park
   LockSupport.park(this);    // park the current thread. this represents the AbstractQueuedSynchronizer object and represents the current thread
   if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
       break;
}

Summary: the first method uses the data structure to insert into the waiting queue;
The second method uses unpark to release the synchronization lock: unparksuccess (H);
The third method uses park to block the current thread: LockSupport.park(this);

3.2 condition.await() source code analysis

The source code of condition.await() is analyzed as follows:
In the first step, the addConditionWaiter() method adds the node to the waiting queue
Step 2: after addConditionWaiter() returns the new node where the current thread is stored, take the node as a parameter of the fullyRelease() method. This fullyRelease() method unlocks the current thread stored in the new node and returns savedState()
Step 3: isOnSyncQueue() provides judgment, LockSupport.park(this); park the current thread (park means blocking, unpark means waking up)

3.2.1 step 1: addConditionWaiter() adds a node to the waiting queue

private Node addConditionWaiter() {
   Node t = lastWaiter;
   if (t != null && t.waitStatus != Node.CONDITION) {
       unlinkCancelledWaiters();
       t = lastWaiter;
   }
   // Create a new node. The node stores the current thread, and the status is set to waiting CONDITION
   Node node = new Node(Thread.currentThread(), Node.CONDITION);
   if (t == null)
       firstWaiter = node;
   else
       t.nextWaiter = node;
   lastWaiter = node;
   return node;
}

addConditionWaiter() adds a node to the waiting queue. There are three situations:

In the first case, there is no node in the current waiting queue (at this time, both firstWaiter and tailWaiter are null)

In the second case, there are 1-n nodes in the current waiting queue (at this time, both firstWaiter and tailWaiter are not null. If there is a node, the head and tail pointers point to the node. If it is greater than a node, the head and tail pointers point to the corresponding node)

In the third case, there are 1-n nodes in the current waiting queue (at this time, neither firstWaiter nor tailWaiter is null. If there is a node, the head and tail pointers point to this node; if it is greater than a node, the head and tail pointers point to the corresponding node), but the node pointed to by the tail pointer is not waiting in the waiting queue (t.waitstatus! = node. Condition)

3.2.1.1 the first case: there are no nodes in the current waiting queue

In the first case, there are no nodes in the current waiting queue (at this time, both firstWaiter and tailWaiter are null). The program in the first case is extracted from the addConditionWaiter method for execution, as follows:

Node t = lastWaiter;  // Because the tail interpolation method should be adopted, the tail pointer should be recorded first
Node node = new Node(Thread.currentThread(), Node.CONDITION);
firstWaiter = node;   // Because there is only the newly created Node in the waiting queue, the head and tail pointers of the waiting queue point to this Node
lastWaiter = node;
return node;   // Returns the new node created by the current thread

The program is executed as above, with a total of five sentences of Java code: first record the lastWaiter, Node t = lastWaiter; (because the tail insertion method is adopted, first record the tail pointer), and then create a new Node using the current thread. The thread attribute of the new Node is the current thread Node node = new Node(Thread.currentThread(), Node.CONDITION); (it means that this Node stores the current thread. Put the current thread into a Node, and then put this Node into the waiting queue), waitStatus=Condition(-2), and then point the head and tail pointers responsible for the waiting queue to this Node (firstWaiter = node; lastWaiter = node;), because there is only this newly created Node in the waiting queue, Finally, return the Node created by the current thread return node;.

3.2.1.2 the second case: 1-n nodes in the current waiting queue

In the second case, there are 1-n nodes in the current waiting queue (at this time, both firstWaiter and tailWaiter are not null. If there is a node, the head and tail pointers point to this node. If it is greater than a node, the head and tail pointers point to the corresponding node). The program execution in the first case is extracted from the addConditionWaiter method, as follows:

Node t = lastWaiter;  // Because the tail interpolation method should be adopted, the tail pointer should be recorded first
Node node = new Node(Thread.currentThread(), Node.CONDITION);
t.nextWaiter = node;  // The classic two steps of tail interpolation method: (1) the next node of the current node is a new node; (2) The pointer at the end of the waiting queue points to the new node
lastWaiter = node;
return node;  // Returns the node newly added to the waiting queue

The program is executed as above, first record the lastWaiter, Node t = lastWaiter; (because the tail insertion method is adopted, first record the tail pointer), and then use the current thread to create a new node Node node = new Node(Thread.currentThread(), Node.CONDITION); Then, the next node of the current node is the new node t.nextWaiter = node;, When pointing to the new node lastWaiter = node; at the end of the waiting queue;, Finally, return the node newly added to the waiting queue return node; (it stores the current thread).

Classic two steps of tail interpolation:
(1) The next node of the current node is a new node t.nextWaiter = node;
(2) The waiting queue tail pointer points to the new node lastWaiter = node;

3.2.1.3 the third case: there are 1-n nodes in the current waiting queue, but the node pointed to by the tail pointer is not waiting in the waiting queue

In the third case, there are 1-n nodes in the current waiting queue (at this time, both firstWaiter and tailWaiter are not null. If there is a node, the head and tail pointers point to this node. If it is larger than a node, the head and tail pointers point to the corresponding node), but the node pointed to by the tail pointer is not waiting in the waiting queue (t.waitstatus! = node. Condition), Extract the program execution of the first case from the addConditionWaiter method, as follows:

Node t = lastWaiter;     // The node pointed to by the record tail pointer is prepared for using the tail interpolation method
unlinkCancelledWaiters();   //Compared with the second special case, it needs to be handled here
t = lastWaiter;   //Compared with the second special case, it needs to be handled here
Node node = new Node(Thread.currentThread(), Node.CONDITION);
t.nextWaiter = node;   // Classic two steps of tail interpolation
lastWaiter = node;
return node;

In the third case, there are seven sentences for executing a Java program, five of which are the same as in the second case. Don't explain. Look at the two new sentences

unlinkCancelledWaiters();   
// Explanation: unbind all waiters in the cancelled state,
// For this, use cancelled to indicate the cancelled status. Here, use waiters to indicate more than one
t = lastWaiter;   // Explanation: reset t, continue to record the node pointed by the new tail pointer, and prepare for the following tail interpolation

Explain the unlikcancelledwaiters () program

private void unlinkCancelledWaiters() {
    Node t = firstWaiter;   // 1. Record the node pointed to by the header pointer in the waiting queue
    // Why does the record header pointer point here? Because the waiting queue is an acyclic single linked list, the while loop deletes the cancelled node and can only traverse from the beginning
    Node trail = null;   // 2. For the local variable trail, keep moving T and use t to record the current node. However, because the waiting list is a single linked list, the previous node of the current node T cannot be recorded. When t has not been moved, record the current t into the trail, and then move t
    while (t != null) {
        Node next = t.nextWaiter;   // 3. Prepare to move, track record t, single linked list basic operations
        if (t.waitStatus != Node.CONDITION) {
            t.nextWaiter = null;   
            if (trail == null)  // 4. This is executed before the execution of trail=t. before the execution of trail=t, keep moving backward and constantly modify the header pointer firstWaiter
                 // 4.1 why should the header pointer firstWaiter be constantly modified before trail=t is executed?
                 //Because t.waitstatus= Node.condition, the current queue is not available, so you should constantly modify the header pointer firstWaiter
                firstWaiter = next;  //The only place to set the header pointer,
                 // 4.2 why not modify the header pointer after trail=t?
                 // Because as long as the t with Node.CONDITION is found, it will not be deleted. It is a reservation operation and a waiting queue that can be used
            else   // This is executed after trail=t
                trail.nextWaiter = next;   //  5. After trail=t is executed, trail - > T - > next, because t.waitstatus= Node. Condition, so to remove T, execute trail.nextWaiter = next; Change to trail - > next
                //5.1 why don't you delete T before executing trail=t, because trail==null at this time
                //5.2 why delete t after trail=t is executed, because when the firstWaiter is determined and the waiting queue is determined, of course, delete the illegal T, t.waitstatus= Node.CONDITION
            if (next == null)  // 6. This is the last time the loop is executed. When next is null, it means that there is nothing left behind. To jump out of the while loop, it is set that the tail pointer points to, but at this time, t.waitstatus= Node.condition, lastWaiter = t cannot be set; Therefore, the front node set to t lastWaiter = trail;   
                lastWaiter = trail;   
        }
        else
            trail = t;   // The last of t is recorded in the trail
        t = next;  // Tmove
    }
}

Explanation of unlikcancelledwaiters() method:

(1) Record the node pointed to by the header pointer in the waiting queue. Node T = firstwait;

(2) Create a new local variable trail, which is used to continuously move T and use t to record the current node. However, because the waiting list is a single linked list, the previous node of the current node T cannot be recorded. When t has not been moved, record the current t into the trail, and then move t to node. Trail = null;

(3) Prepare to move. The trail records T. This is the basic operation of the single linked list: the first sentence in the while loop is Node next = t.nextWaiter, The last sentence is t = next, The combination is t= t.nextWaiter; Indicates that each while loop is moved once t

(4) Before trail = t is executed, keep moving backward and constantly modify the header pointer firstWaiter
① Why should the header pointer firstWaiter be constantly modified before trail=t is executed? Because t.waitstatus= Node.condition, the current queue is not available, so you should constantly modify the header pointer firstWaiter.
② Why not modify the header pointer after executing trail=t? As long as the T with Node.CONDITION is found, it will not be deleted. It is a reservation operation and a waiting queue that can be used.

(5) After trail=t is executed, the linked list changes to trail - > T - > next because t.waitstatus= Node. Condition, so to remove T, execute trail.nextWaiter = next; Change the linked list to trail - > next
① Why don't you delete T before trail=t? Because at this time, trail==null.
② Why delete t after trail=t? Because at this time, the firstWaiter is determined and the waiting queue is determined. Of course, delete the illegal T, t.waitstatus= Node.CONDITION.

(6) if (next == null) this is the last time the loop is executed. When next is null, it means there is nothing left behind. To jump out of the while loop, it is set that the tail pointer points to, but at this time, t.waitstatus= Node.condition, lastWaiter = t cannot be set; Therefore, the front node set to t lastWaiter = trail;

Now let me look at a basic problem of data structure. As a single linked list, the waiting queue has only the next pointer and no prev pointer. How does it record the last node.

Question (basic knowledge of single linked list): how does trail record the previous node of t
Answer: if the code is written as t= t.nextWaiter; If the T pointer is moved directly, the trail cannot record the previous node of T, so we consider the following:
Step 1: split the movement of T, t= t.nextWaiter; Become

Node next = t.nextWaiter;
trail = t;
t = next; 

The first and third sentences are actually t= t.nextWaiter; We split it up. When the value of T has not been modified, in the second sentence, execute trail=t and record t.

Step 2: finally, put the three sentences in the same while loop to synchronize in real time

 while (t != null){
    Node next = t.nextWaiter;
    trail = t;
    t = next; 
 }

3.2.2 step 2: the fullyRelease() method unlocks the current thread stored in the new node

addConditionWaiter() adds a new node to the waiting queue and returns the new node where the current thread is stored. Then, take the node as a parameter of the fullyRelease() method. This fullyRelease() method unlocks the current thread stored in the new node and returns savedState()

final int fullyRelease(Node node) {
    boolean failed = true;   // Set the flag bit to fail by default. Why did it fail by default at the beginning? Because it was not executed at the beginning, it must be set to successful after execution
    try {
        int savedState = getState();  // Get the current class variable state, which is used to record the locking times of the current thread (because synchronized and lock are reentrant locks, they can be locked multiple times)
        if (release(savedState)) {  // If the current thread is released successfully, the synchronization lock must be released to block or enter the waiting queue
            failed = false;  // If the current thread is released successfully, the failed flag bit is set to false
            return savedState;   // Returns the number of times the current thread has been locked. A value of 0 means that the lock has been completely unlocked
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed) // If the failed flag bit is true, it indicates that the lock release fails and the node status is set to cancelled
            node.waitStatus = Node.CANCELLED;
    }
}

Explain the fullyRelease method as follows:
(1) Set the flag bit to failed by default, boolean failed = true, Why fail by default at the beginning? Because it was not executed at the beginning, it must be set to successful after execution.
(2) Get the current class variable state int savedState = getState(), This state is used to record the locking times of the current thread (because synchronized and lock are reentrant locks, they can be locked multiple times).
(3) If the current thread is released successfully (if (release(savedState)), to block, you need to enter the waiting queue and release the synchronization lock.
(4) If the current thread is released successfully, the failed flag bit is set to false, failed = false;, And returns the number of times the current thread has been locked. A value of 0 means that it has been completely unlocked. return savedState.
(5) If the failed flag bit is true if (failed), it indicates that the lock release has failed, and the node status is set to cancelled node.waitStatus = Node.CANCELLED.

This fullyRelease method calls the release method to release the lock. Let's take a look at the release method, as follows:

public final boolean release(int arg) {  // The parameter is the number of times the thread is locked
    if (tryRelease(arg)) {   // Release the synchronization lock. As mentioned in the previous blog, the parameter is the number of times the thread locks
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);   
        return true;
    }
    return false;
}

The core method of unlocking is the release () method. The fullyRelease() method only calls the release () method. Finally, the release () method provides judgment, and tryRelease() only provides judgment to the release () method. There are three situations for the logic of release() to release the synchronization lock:

Case 1: only one thread is locked and no AQS queue is formed

In this concurrency process, only one thread is locked, so the AQS queue is not created. Here, if judgment is not tenable, that is, tryRelease() is judged to be false, and the release() method directly returns false;

Case 2: two threads are locked to form an AQS queue. When thread B is unlocked

In the process of concurrent locking (i.e. lock.lock() in the previous blog), thread A successfully locks and thread B locks, but now thread A is not unlocked. At this time, an AQS queue is formed (that is, A locking queue. Thread A and thread B are locked here. Thread A is in front of thread B, which is in the previous blog). Then thread A unlocks, Only thread B is left in the AQS queue, and then thread B unlocks it. At this time, thread B is the first element of the AQS queue. At this time, the value of waitStatus of thread B is 0, and if in if will not be executed (with AQS queue, you can use the first if (tryRelease(arg)), but if (H! = null & & h.waitStatus! = 0) When judging, H! = null and h.waitStatus == 0, so the whole method cannot return true through the second if).

Case 3: two threads are locked to form an AQS queue. When thread A is unlocked

In the process of concurrent locking (lock.lock() in the previous blog), thread A successfully locks and thread B locks, but now thread A is not unlocked. At this time, an AQS queue is formed (that is, A locking queue. Thread A and thread B are locked here. Thread A is in front of thread B, which is in the previous blog). Then thread A unlocks first, At this time, thread A is the queue head element of the AQS queue. Since there are two elements of thread A and thread B in the AQS queue, the value of waitStatus of thread A is not 0. If in if executes, unparksuccess (H); Unlock the next node of the head node (that is, unlock thread b), and the whole method returns true.

Finally, we will explain tryRelease(), which is very simple, as follows:

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;    // Number of thread locks - number of thread locks = 0
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;   // The default release success is false
    if (c == 0) {  // The number of locks is 0
        free = true;    // Flag bit successfully releases synchronization lock
        setExclusiveOwnerThread(null);  // Exclusive thread is null
    }
    setState(c);  // Reset the number of times the class variable state thread locks
    return free;
}

To summarize, there are three methods fullyRelease(), release(), and tryRelease(): the calling link is fullyRelease() - > release() - > tryRelease(). Among the three methods, the release() method is the core method of unlocking. The fullyRelease() method only calls the release() method, and the final judgment is provided by the release() method. tryRelease() is only for this release() Method provides judgment.

3.2.3 step 3: block the current thread

isOnSyncQueue() provides judgment, LockSupport.park(this); Park the current thread (explanation: Park means blocking, unpark means waking up)

final boolean isOnSyncQueue(Node node) {
    // It is currently waiting and there are no precursors in the synchronization queue  
    if (node.waitStatus == Node.CONDITION || node.prev == null)   
        return false;
    // If this node has a successor in the synchronization queue, it must be in the synchronization queue,
    // prev and next are pointers to the synchronization queue, and nextWaiter is a pointer to the waiting queue
    if (node.next != null) 
        return true;
    // The above two conditions are not met, and both are else  
    // node.next==null node.prev!=null&&node.waitStatus != Node.CONDITION
    return findNodeFromTail(node);  
}
// If you can enter this method, it must be node. Next = = null node. Prev= null,
// Therefore, in the synchronization queue, traverse from the back to the front, find this node and return true. If it is not found until the front, return false
private boolean findNodeFromTail(Node node) {   
    Node t = tail;
    for (;;) {
        if (t == node)
            return true;
        if (t == null)
            return false;
        t = t.prev;
    }
}

Finally, execute LockSupport.park(this) to park the current thread (explanation: Park is blocking and unpark is wake-up).

4, signal()/signalAll() source code

4.1 overall flow chart of condition. Notify()

Let's take a look at the source code of signal method and signalAll method

When a thread calls the signal method or signalAll method, the general steps are as follows:

Step 1: put the nodes in the waiting queue into the AQS synchronous execution queue (thread is stored in each Node node). Specifically, the signal method will add the Node of the first waiting thread in the current waiting queue to the original AQS queue, The signalAll method is to add all the nodes of the waiting thread in the waiting queue to the original AQS queue.

The second step is to acquire the synchronization lock: let them acquire the lock again and unpark.

Step 3: wake up the current thread: the thread is awakened and executes the code in the corresponding thread.

4.2 source code analysis of condition. Notify()

4.2.1 the nodes in the waiting queue are placed in the AQS synchronous execution queue

The function of condition.notify() method: put the nodes in the waiting queue into the AQS synchronous execution queue, in which thread threads are stored in each Node

The condition.notify() method has two steps
Step 1: wait for the first node in the queue to be removed
Step 2: add this node to the synchronization queue

4.2.1.1 remove the first node from the waiting queue

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;    // Record the node pointed to by the header pointer in the waiting queue, because we want to delete the first node in the waiting queue
    if (first != null)
        doSignal(first);
}
private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)   // The next waiting element is empty, which means that there is only one element in the waiting queue. Because this element is removed, it must be set to null. The pointer at the end of the waiting queue lastWaiter = null;
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) && (first = firstWaiter) != null);
}

Explanation of doSignal() method: for the queue of non cyclic one-way linked list, the linked list header element should be deleted
1. You need to modify the waiting queue header pointer to point to the next node of the current waiting queue, that is, execute firstwait = first.nextwaiter
2. You need to empty the nextWaiter pointer of the first node of the current waiting queue, that is, execute first.nextWaiter = null;

There are two cases:
In the first case, there is only one node in the waiting queue
In the second case, there are 2-n nodes in the waiting queue

In the first case, if there is only one node in the waiting queue, the execution process is:

firstWaiter = first.nextWaiter   // You need to modify the waiting queue header pointer to point to the next node of the current waiting queue
lastWaiter = null;    // No more elements, null the pointer to the end of the waiting queue
first.nextWaiter = null;   // You need to empty the nextWaiter pointer of the first node of the current waiting queue

In the second case, if there are 2-n nodes in the waiting queue, the execution process is as follows:

firstWaiter = first.nextWaiter  // You need to modify the waiting queue header pointer to point to the next node of the current waiting queue
first.nextWaiter = null;  // You need to empty the nextWaiter pointer of the first node of the current waiting queue

transferForSignal(first) method. The actual parameter is the first node at the head of the waiting queue, which indicates the node deleted at the head of the waiting queue and the node added at the end of the synchronization queue

transferForSignal(first) inserts the node removed from the waiting queue into the synchronization queue by tail interpolation, so directly pass the first node as a parameter to the transferForSignal() operation. Remember, this first node is the node removed from the waiting queue

final boolean transferForSignal(Node node) {
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    Node p = enq(node);  // Insert the node node, which is removed by the waiting queue, into the synchronization queue
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
} 

4.2.1.2 add this node in the synchronization queue

Adding this node in the synchronization queue uses the enq() method, which contains two cases,
Case 1: there is no node in the synchronization queue, so tail == null
Case 2: there are nodes in the synchronization queue, so tail= null

The first case: there is no node in the synchronization queue, so tail==null. The program execution is as follows:

Node t = tail;  // There is no node in the synchronization queue, tail==null        
compareAndSetHead(new Node())  // 
tail = head;
t=tail;   // Re update tail node record
 // Three steps of insertion operation  
node.prev = t;
compareAndSetTail(t, node)
t.next = node;
return t;   // Return the node in front of the tail node. There are two nodes t node in the current synchronization queue. Now return t

The second case: there are nodes in the synchronization queue, so tail= Null, the program executes as follows:

Node t = tail;  // There is no node in the synchronization queue, tail= null        
 // Three steps of insertion operation  
node.prev = t;
compareAndSetTail(t, node)
t.next = node;
return t;   // Return the node in front of the tail node. There are n nodes in the current synchronization queue, node1, node2... T node. Now return t

Question 1: why does the enq() method return the state of the previous node of the tail node?
Answer 1: because the previous node of the tail node is the tail node before insertion. All the meaning of enq lies in two points. Insert the new node specified by the parameter + return to the original tail node.

Question 2: why is it necessary to create a new node when tail==null in enq() method?
Answer 2: follow the above, because the meaning of enq lies in two points. Insert the new node specified by the parameter + return to the original tail node, because to return to the original tail node, if there is no original tail node, create a new node as the original tail node to serve the return value.

4.2.2 obtaining synchronization lock

After the enq() method adds a node to the end of the synchronization queue, the loop detection in the await() method soon detects that there are just blocked nodes in the synchronization queue before the transferForSignal() is executed (that is, the newly blocked node comes out of the blocking queue and goes to the synchronization queue. All threads in this node can compete for synchronization locks. Try acquire)

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {   // When there is this node in the synchronization queue
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // acquireQueued(node, savedState)
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {  // tryAcquire successfully obtained the synchronization lock
                setHead(node);    
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

4.2.3 wake up the current thread

Next, after the blocked node in the await() method is released, it can participate in the preemption of the synchronization lock. After the CAS operation succeeds in preempting the synchronization lock, the transferForSignal() method wakes up the current thread of the node node, that is, execute the LockSupport.unpark(node.thread) statement.

final boolean transferForSignal(Node node) {
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    Node p = enq(node);  // Insert the node node, which is removed by the waiting queue, into the synchronization queue and return to the previous node of the tail node
    int ws = p.waitStatus;  // The state of the previous node of the tail node and the state of the previous tail node
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))  // If the previous tail node ws==1, it indicates that the previous node has been cancelled, or the cas of the previous tail node failed to set the waiting state
        LockSupport.unpark(node.thread);   // Wake up the node thread to resynchronize  
    return true;
}

5, Interview golden finger

5.1 ConditionObject class and Node class

AQS is essentially an acyclic two-way linked list (also known as queue), so it is composed of nodes, that is, nodes. The following lock() unlock() await() signal()/signalAll() operate with nodes as the basic elements. What information needs to be saved in this Node class?
Answer: there are seven Node attributes (focusing on the first five)

volatile int waitStatus; //Current node waiting state
volatile Node prev; //Previous node
volatile Node next; //Next node
volatile Thread thread; //Value in node
Node nextWaiter; //Next waiting node
static final Node SHARED = new Node();  //Indicates whether the node is shared or exclusive. The default initial is shared
static final Node EXCLUSIVE = null;

(1) The attributes (locking and unlocking) used in the synchronization queue include: next, prev, thread and waitStatus. Therefore, the synchronization queue is a two-way acyclic linked list. The class variables involved, head and tail in the AbstractQueuedSynchronizer class, point to the head node and tail node in the synchronization queue respectively.

(2) The attributes (blocking and wake-up) used in the waiting queue include nextWaiter, thread and waitStatus. Therefore, the waiting queue is a one-way acyclic linked list. The class variables involved, firstWaiter and lastWaiter in the ConditionObject class, point to the head node and tail node in the waiting queue respectively.

(3) AQS queues are work queues, synchronous queues and acyclic two-way queues: when head tail is used, AQS queues are established. A single thread does not use head tail, so AQS queues are not established;

(4) The waiting queue is an acyclic one-way queue: when the firstwait lastwait is used, the waiting queue is established.

(5) lock() and unlock() are the operation synchronization queue: lock() encapsulates the thread into the node (at this time, the attributes used by the node are thread, nextWaiter and waitStatus) and puts them into the synchronization queue / AQS queue. unlock() takes the node storing the thread out of the synchronization queue, indicating that the thread has completed its work.

(6) await() and signal() are the operation waiting queue: await() encapsulates the thread into the node (at this time, the attribute used by the node is thread prev next waitStatus), puts it into the waiting queue, and signal() takes out the elements from the waiting queue.

Question: Why are the head and tail responsible for synchronizing the queue in the AbstractQueuedSynchronizer class, but the firstWaiter and lastWaiter responsible for waiting the queue in the ConditionObject class?
answer:
(1) For thread synchronization mutual exclusion, it is directly implemented through ReentrantLock class objects lock.lock(), lock.unlock(), and ReentrantLock class objects call AQS class to unlock locks. Therefore, the head and tail responsible for synchronization queue are in AbstractQueuedSynchronizer class;
(2) For thread blocking and wake-up, a correspondence is obtained through the ReentrantLock class object lock.newCondition(), the condition reference points to this object, and then the condition. Await() and condition. Signal() are implemented. Therefore, the firstWaiter and lastWaiter responsible for waiting for the queue are in the ConditionObject class.

5.2 condition.await()

5.2.1 condition.await() three steps

General process: the first method is inserted into the waiting queue, the second method releases the synchronization lock, and the third method blocks the current thread. The three are integrated and cannot be separated. The second method releases the synchronization lock before the third method, and then suspends the thread. Purpose: to avoid being suspended when the current thread does not release the lock, resulting in other threads being blocked The lock cannot be retrieved, or a deadlock occurs.

Overall process details:

In the first step, if a thread calls the await method, it will add the waiting thread to the AQS waiting queue through CAS and tail interpolation (explanation: the method of CAS and tail interpolation means that when CAS ensures thread safety, the tail interpolation method is used to put the thread into a Node node in three steps and insert it into the AQS waiting queue). The corresponding code is Node node = addConditionWaiter();

Step 2: Unlock the current thread (explanation: the purpose of unlocking the current thread is to prevent it from being suspended when the thread does not release the lock, resulting in other threads not obtaining the lock or causing deadlock). The corresponding code is int savedState = fullyRelease(node);

Step 3: park the current thread (explanation: after Park, this thread can only passively wait for other threads to call the signal method to unpark the current thread), and the corresponding code is LockSupport.park(this);

Summary: the first method uses the data structure to insert into the waiting queue,
The second method uses unpark to release the synchronization lock: unparksuccess (H);
The third method uses park to block the current thread: LockSupport.park(this);

5.2.2 addConditionWaiter() adds a node to the waiting queue

In the first case, there is no node in the current waiting queue (at this time, both firstWaiter and tailWaiter are null)

The procedure is as follows:

Node t = lastWaiter;  // Because the tail interpolation method should be adopted, the tail pointer should be recorded first
Node node = new Node(Thread.currentThread(), Node.CONDITION);
firstWaiter = node;   // Because there is only the newly created Node in the waiting queue, the head and tail pointers of the waiting queue point to this Node
lastWaiter = node;
return node;   // Returns the new node created by the current thread

In the second case, there are 1-n nodes in the current waiting queue (at this time, both firstWaiter and tailWaiter are not null. If there is a node, the head and tail pointers point to the node. If it is greater than a node, the head and tail pointers point to the corresponding node)

The execution procedure is as follows:

Node t = lastWaiter;  // Because the tail interpolation method should be adopted, the tail pointer should be recorded first
Node node = new Node(Thread.currentThread(), Node.CONDITION);
t.nextWaiter = node;  // There are two classic steps in tail interpolation: (1) the next node of the current node is a new node; (2) the tail pointer of the waiting queue points to the new node
lastWaiter = node;
return node;  // Returns the node newly added to the waiting queue

In the third case, there are 1-n nodes in the current waiting queue (at this time, neither firstWaiter nor tailWaiter is null. If there is a node, the head and tail pointers point to this node; if it is greater than a node, the head and tail pointers point to the corresponding node), but the node pointed to by the tail pointer is not waiting in the waiting queue (t.waitstatus! = node. Condition)

The execution procedure is as follows:

Node t = lastWaiter;     // The node pointed to by the record tail pointer is prepared for using the tail interpolation method
unlinkCancelledWaiters();   //Compared with the second special case, it needs to be handled here
t = lastWaiter;   //Compared with the second special case, it needs to be handled here
Node node = new Node(Thread.currentThread(), Node.CONDITION);
t.nextWaiter = node;   // Classic two steps of tail interpolation
lastWaiter = node;
return node;

The execution procedure is as above. There is no problem. Look at the two new sentences

unlinkCancelledWaiters();   
// Explanation: unbind all waiters in the cancelled state,
// For this, use cancelled to indicate the cancelled status. Here, use waiters to indicate more than one
t = lastWaiter;   // Explanation: reset t, continue to record the node pointed by the new tail pointer, and prepare for the following tail interpolation

In particular, explain the unlikcancelledwaiters() program

private void unlinkCancelledWaiters() {
    Node t = firstWaiter;   // 1. Record the node pointed to by the header pointer in the waiting queue
 // Why does the record header pointer point here? Because the waiting queue is an acyclic single linked list, the while loop deletes the cancelled node and can only traverse from the beginning
    Node trail = null;   // 2. For the local variable trail, keep moving T and use t to record the current node. However, because the waiting list is a single linked list, the previous node of the current node T cannot be recorded. When t has not been moved, record the current t into the trail, and then move t
    while (t != null) {
        Node next = t.nextWaiter;   // 3. Prepare to move, track record t, single linked list basic operations
        if (t.waitStatus != Node.CONDITION) {
            t.nextWaiter = null;   // 
            if (trail == null)  // 4. This is executed before the execution of trail=t. before the execution of trail=t, keep moving backward and constantly modify the header pointer firstWaiter
                 // 4.1 why should the header pointer firstWaiter be constantly modified before trail=t is executed?
                 //Because t.waitstatus! = node.condition, the current queue is not available, so we should constantly modify the header pointer firstWaiter
                firstWaiter = next;  //The only place to set the header pointer,
                 // 4.2 why not modify the header pointer after trail=t?
                 // Because as long as the t with Node.CONDITION is found, it will not be deleted. It is a reservation operation and a waiting queue that can be used
            else   // This is executed after trail=t
                trail.nextWaiter = next;   //  5. After executing trail=t, trail - > T - > next. Because t.waitstatus! = node.condition, to remove T, execute trail.nextWaiter = next; change to trail - > next
                //5.1 why don't you delete T before executing trail=t, because trail==null at this time
                //5.2 why delete t after trail=t is executed? Because when the firstWaiter is determined and the waiting queue is determined, of course, delete the illegal T, t.waitstatus! = node. Condition
            if (next == null)  // 6. This is the last time the loop is executed. When next is null, it means there is nothing left behind. To jump out of the while loop, it is set that this is the tail pointer, but at this time t.waitstatus! = node.condition, lastWaiter = t cannot be set; therefore, the front node lastWaiter = trail set to t;   
                lastWaiter = trail;   
        }
        else
            trail = t;   // The last of t is recorded in the trail
        t = next;  // Tmove
    }
}

5.2.3 release the synchronization lock

The core method of unlocking is the release() method (fullyRelease() method only calls the release() method, which provides judgment, and tryRelease() only provides judgment to the release() method):

There are three situations for the logic of release() to release the synchronization lock:

1. Only one thread is locked and no AQS queue is formed:
In this concurrency process, only one thread is locked, so the AQS queue is not created. Here, if judgment is not tenable, that is, tryRelease() is judged to be false, and the release() method directly returns false;

2. Two threads are locked to form an AQS queue. When thread B is unlocked:
In the process of concurrent locking (i.e. lock.lock() in the previous blog), thread A successfully locks and thread B locks, but now thread A is not unlocked. At this time, an AQS queue is formed (tip: that is, A locking queue, thread A and thread B are locked here, thread A is in front of thread B, which is in the previous blog), Then thread A unlocks, and only thread B remains in the AQS queue. Then thread B unlocks. At this time, thread B is the first element of the AQS queue. At this time, the value of waitStatus of thread B at the head of the queue is 0, and if in if will not be executed (Tip: with AQS queue, you can pass through the first if (tryRelease(arg)), but if (H! = null & & h.waitStatus! = 0) When judging, H! = null, h.waitStatus = = 0, so the whole method cannot return true through the second if).

3. Two threads are locked to form an AQS queue. When thread A is unlocked:
In the process of concurrent locking (i.e. lock.lock() in the previous blog), thread A successfully locks and thread B locks, but now thread A is not unlocked. At this time, an AQS queue is formed (tip: that is, A locking queue, thread A and thread B are locked here, thread A is in front of thread B, which is in the previous blog), and then thread A unlocks first, At this time, thread A is the queue head element of the AQS queue. Since there are two elements of thread A and thread B in the AQS queue, the value of waitStatus of thread A is not 0. If in if executes, unparksuccess (H); Unlock the next node of the head node (tip: Unlock thread b), and the whole method returns true.

5.2.4 blocking threads

final boolean isOnSyncQueue(Node node) {
    // It is currently waiting and there are no precursors in the synchronization queue  
    if (node.waitStatus == Node.CONDITION || node.prev == null)   
        return false;
    // If this node has a successor in the synchronization queue, it must be in the synchronization queue,
    // prev and next are pointers to the synchronization queue, and nextWaiter is a pointer to the waiting queue
    if (node.next != null) 
        return true;
    // The above two conditions are not met, and both are else  
    // node.next==null node.prev!=null&&node.waitStatus != Node.CONDITION
    return findNodeFromTail(node);  
}
// If you can enter this method, it must be node. Next = = null node. Prev= null,
// Therefore, in the synchronization queue, traverse from the back to the front, find this node and return true. If it is not found until the front, return false
private boolean findNodeFromTail(Node node) {   
    Node t = tail;
    for (;;) {
        if (t == node)
            return true;
        if (t == null)
            return false;
        t = t.prev;
    }
}

Finally, execute LockSupport.park(this) to park the current thread (explanation: Park means blocking and unpark means unlocking).

5.3 condition.notify()

5.3.1 three steps of condition. Notify()

In the first step, the nodes in the waiting queue are placed in the AQS synchronous execution queue. The thread thread is stored in the Node node. The signal method adds the Node of the first waiting thread in the current waiting queue to the original AQS queue, while the signalAll method adds all the nodes of all waiting threads in the waiting queue to the original AQS queue

Step 2: obtain the synchronization lock

Step 3: wake up the current thread

5.3.2 the nodes in the waiting queue are placed in the AQS synchronous execution queue

5.3.2.1 delete nodes in waiting queue

There are two cases:

If there is only one node in the waiting queue

firstWaiter = first.nextWaiter   // You need to modify the waiting queue header pointer to point to the next node of the current waiting queue
lastWaiter = null;    // No more elements, null the pointer to the end of the waiting queue
first.nextWaiter = null;   // You need to empty the nextWaiter pointer of the first node of the current waiting queue

If there are 2-n nodes in the waiting queue

firstWaiter = first.nextWaiter  // You need to modify the waiting queue header pointer to point to the next node of the current waiting queue
first.nextWaiter = null;  // You need to empty the nextWaiter pointer of the first node of the current waiting queue

5.3.2.2 adding nodes by synchronous queue tail interpolation

First node: the node deleted at the head of the waiting queue and added at the end of the synchronization queue: transferForSignal(first) to insert the node removed from the waiting queue into the synchronization queue using the tail interpolation method, so directly pass the first node as a parameter to the transferForSignal() operation. Remember, this first node is the node removed by the waiting queue

Add this node to the synchronization queue: enq()
For enq() two cases,
1. There is no node in the synchronization queue, so tail==null
2. There are nodes in the synchronization queue, so tail= null

The first case: there is no node in the synchronization queue, so tail==null

The procedure is as follows:

Node t = tail;  // There is no node in the synchronization queue, tail==null        
compareAndSetHead(new Node())  // 
tail = head;
t=tail;   // Re update tail node record
 // Three steps of insertion operation  
node.prev = t;
compareAndSetTail(t, node)
t.next = node;
return t;   // Return the node in front of the tail node. There are two nodes t node in the current synchronization queue. Now return t

The second case: there are nodes in the synchronization queue, so tail= null

The procedure is as follows:

Node t = tail;  // There is no node in the synchronization queue, tail= null        
 // Three steps of insertion operation  
node.prev = t;
compareAndSetTail(t, node)
t.next = node;
return t;   // Return the node in front of the tail node. There are n nodes in the current synchronization queue, node1, node2... T node. Now return t

Question 1: why does the enq() method return the state of the previous node of the tail node?
Answer 1: because the previous node of the tail node is the tail node before insertion. All the meaning of enq lies in two points. Insert the new node specified by the parameter + return to the original tail node

Question 2: why is it necessary to create a new node when tail==null in enq() method?
Answer 2: follow the above, because the meaning of enq lies in two points. Insert the new node specified by the parameter + return to the original tail node, because you want to return to the original tail node. If there is no original tail node, you need to create a new node as the original tail node to serve the return value

5.3.3 obtaining synchronization lock

After the enq() method adds nodes to the work queue, the loop detection in the await() method quickly detects that there are just blocked nodes in the synchronization queue before the transferForSignal() is executed (that is, the newly blocked node comes out of the blocking queue and goes to the synchronization queue. All threads in this node can compete for synchronization locks. Try acquire)

5.3.4 wake up the current thread

After await() preempts the synchronization lock, the transferForSignal() method wakes up the current thread of the node node

6, Epilogue

Blocking and waking up, waiting for the stage of the queue, completed.

Make progress every day!!!

Tags: Big Data Spark

Posted on Sun, 28 Nov 2021 08:51:46 -0500 by Jurik