Concurrent Programming-Detailed Explanation of the Use and Principle of Condition

I. Overview

Condition itself is also an interface, and its function is similar to wait/notify in the Object class. The wait and notify methods in Object need to be used in conjunction with the synchronized keyword to realize the wait/notify function between threads. The Condition interface also provides similar functions, but it needs to be used in conjunction with Lock to realize the wait/notify mode.

Two, Condition interface and examples

 Condition defines two types of waiting/notifying methods. When the current thread calls these methods, it needs to acquire the lock associated with the Condition object in advance. The Condition object is created by calling the newCondition() method of the Lock object, that is, the Condition is dependent on the Lock object.

The use of Condition is very simple, you need to pay attention to acquiring the lock before calling the method. Let's learn about the use of Condition through an example of bounded confrontation. Bounded queue means that when the queue is empty, the acquisition slot will be blocked until there are new elements in the queue. When the queue is full, the queue insertion operation will be blocked until the queue is empty. The code is as follows:

public class SyncBoundedQueue<E> {

    private LinkedList<E> items;
    private int size; // 记录队列的初始大小
    private Lock lock = new ReentrantLock(); //定义一个锁
    private Condition notFull = lock.newCondition(); //队列不满
    private Condition notEmpty = lock.newCondition(); // 队列不空

    public SyncBoundedQueue(int size){
        items = new LinkedList<>();
        this.size = size;
    }
    //添加元素
    public void add(E e) throws InterruptedException{
        lock.lock();
        try {
            // 当队列已满时,需要阻塞等待,等待其他线程获取元素,流程空位
             while (items.size() == size){
                 notFull.await();
             }
             items.addLast(e); // 有空位则添加元素
             notEmpty.signalAll(); // 通知获取元素的线程现在队列已经有元素了
        }finally {
            lock.unlock();
        }
    }

    // 获取元素
    public E get() throws InterruptedException{
        lock.lock();
        try {
            // 如果队列中没有元素,则一直等待
             while (items.size() == 0){
                 notEmpty.await();
             }
             E e = items.removeFirst(); // 移除元素
             notFull.signalAll(); // 移除元素有空位,通知唤醒添加元素的线程
             return e; // 返回元素
        }finally {
            lock.unlock();
        }
    }
}

Three, realization principle analysis

1. Basic principle analysis

Because Condition must be used with Lock, the implementation of Condition is also part of Lock. Let's take a look at the construction of the Condition in the mutex.

    // ReentrantLock 中的方法
    public Condition newCondition() {
        return sync.newCondition(); 
    }

   abstract static class Sync extends AbstractQueuedSynchronizer {
        .....

        final ConditionObject newCondition() {
            return new ConditionObject(); // 创建 Condition 对象
        }

        .....
    }

As you can see from the above code, calling the Lock.newConditon() method is actually calling the method in the Sync inner class that creates the Condition and implements the ConditionObject(). The ConditionObject class is an internal class of the synchronizer AQS, because the operation of the Condition requires an associated lock, and multiple threads are blocked on each Condition object. Therefore, there is also a queue composed of doubly linked lists inside ConditionObject, as shown below:

    public class ConditionObject implements Condition {

        private transient Node firstWaiter;
      
        private transient Node lastWaiter;
	}

 It can be seen from the code implementation that the waiting queue inside ConditionObject is a FIFO queue. Each node in the queue contains a thread reference. The thread is a waiting thread on the Condition object. If a thread calls the Condition.await() method , Then the thread will release the lock, construct a node to join the waiting match and enter the waiting state. A Condition contains a waiting queue, and the Condition has a first node (firstWaiter) and an end node (lastWaiter ). When the current thread calls the Condition.await() method, the node will be constructed with the current thread, and the node has never been added to the waiting queue. The basic structure u of the waiting queue is as follows:

The basic structure of the waiting queue

In the monitor model of Object, an object has a synchronization queue and a waiting queue, and Lock has a synchronization queue and multiple waiting queues, as shown in the following figure:

Synchronization queue and waiting queue

 

2. Waiting for analysis

Calling Condition's await method will cause the current thread to enter the waiting queue and release the lock, and at the same time the thread state becomes the waiting state. For example, the wait method is implemented as follows:

        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)) { // 判断当前节点是否在同步队列中,如不不存在则进行等待阻塞
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE) // 重新竞争锁
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode); // 如果线程已被中断,在唤醒后抛出中断异常
        }

Regarding await, there are a few key points to explain:

(1) When the thread calls await(), it must have acquired the lock first. Therefore, within addConditionWaiter(), the enqueue operation does not need to perform CAS operations, and the thread is inherently safe.

(2) Before the thread executes the wait operation, the lock must be released. That is, fullyRelease(node), otherwise deadlock will occur. This is the same as the coordination mechanism of wait/notify and synchronized.

(3) After the thread is awakened from wait, it must use the acquireQueued(node, savedState) function to take the lock again.

(4) The checkInterruptWhileWaiting(node) code is after the park(this) code to detect whether an interrupt signal has been received during the park. When a thread wakes up from the park, there are two possibilities: one is that another thread calls unpark, and the other is that it receives an interrupt signal. The await() function here can respond to interrupts, so when you find that you are awakened by an interrupt instead of being awakened by unpark, you will directly exit the while loop, and the await() function will also return.

(5) isOnSyncQueue (node) is used to determine whether the Node is in the synchronization queue of AQS. Initially, Node is only in the Condition queue, not in the AQS queue. But when the signal operation is executed, it will be placed in the AQS synchronization queue.

The process is shown in the figure below:

The current thread joins the waiting queue

3. Notification implementation analysis

Calling the signal() method of Condition will wake up the node that has been waiting for the longest time in the waiting queue, and move the node to the synchronization queue before waking up. As shown in the following code:

        public final void signal() {
            if (!isHeldExclusively()) // 在调用 signal 方法之前必须先获取到锁
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first); // 唤醒第一个节点
        }

        private void doSignal(Node first) { //唤醒第一个节点
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

    final boolean transferForSignal(Node node) {
  
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

       
        Node p = enq(node); // 关键点:先把唤醒的节点加入到同步队列中,然后唤醒
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread); // 唤醒当前线程
        return true;
    }

Same as await(), when calling signal(), you must get the lock first (otherwise the above exception will be thrown), because the lock was released when await() was executed earlier. Then, take firstWait from the queue and wake it up. Before waking it up by calling unpark, first use the enq (node) function to put this Node into the blocking queue corresponding to the AQS lock. It is precisely because of this that there is the judgment condition while (!isOnSyncQueue(node)) in the await() function. This judgment condition is met, indicating that the await thread is not interrupted, but awakened by unpark.

The node moves from the waiting queue to the synchronization queue as shown in the following figure:

The node waits for the queue to move to the synchronization queue

 

references:

"Java Concurrency Implementation Principle: Analysis of JDK Source Code"

"The Art of Concurrent Programming in Java"

"Java Concurrent Programming Practice"

 

Guess you like

Origin blog.csdn.net/small_love/article/details/111409675