Principle Analysis of AbstractQueuedSynchronizer - Principle of Condition Implementation

1 Introduction

ConditionIs an interface, the inner class in AbstractQueuedSynchronizer ConditionObjectimplements this interface. ConditionDeclares a set 等待/通知of methods that function similarly to methods such as Objectin . wait/notify/notifyAllThe same thing between the two is that the 等待/通知methods they provide are for the running order of co-threads. However, the method in Object needs to be used with the synchronized keyword, while the method in Condition needs to be used with the lock object, and the newConditionimplementation class object is obtained through the method. In addition, the methods declared in the Condition interface are more functionally rich. For example, Condition declares a wait interface that does not respond to interrupts and timeouts, which are not available in the Object wait method.

This article is the continuation of the previous article AbstractQueuedSynchronizer Principle Analysis - Exclusive/Shared Mode . Before learning the principle of Condition, it is recommended that you first understand the principles of AbstractQueuedSynchronizer synchronization queue. This article will involve knowledge about synchronization queues, which was analyzed in the previous article.

About Conditionthe introduction, let's talk about this first, and then analyze the principle of the Conditionimplementation class .ConditionObject

2. Implementation principle

ConditionObjectWaiting threads are managed through a conditional queue based on a singly linked list. When a thread calls a awaitmethod to wait, it releases the synchronization state. At the same time, the thread will be encapsulated into a waiting node, and the node will be placed at the tail of the condition queue to wait. signalWhen a thread calls or method in the case of acquiring an exclusive lock singalAll, the waiting thread in the queue will be awakened and compete for the lock again. In addition, it should be noted that a lock object can create multiple ConditionObject objects at the same time, which means that multiple threads competing for the same exclusive lock can wait in different condition queues. On wakeup, a thread in the specified condition queue can be woken up. Its general schematic diagram is as follows:

The above is the general principle of the waiting/notification mechanism implemented by ConditionObject, and it is not difficult to understand. Of course, in a specific implementation, more detailed consideration is required. The details will be explained in the next chapter, so keep reading.

3. Source code analysis

3.1 Wait

Several different wait methods are implemented in ConditionObject, each with its own characteristics. For example await(), it will respond to interrupts, but awaitUninterruptibly()not respond to interrupts. await(long, TimeUnit)On the basis of responding to interrupts, a timeout function will be added. In addition, there are some waiting methods, which are not listed here.

In this section, I will mainly analyze await()the method implementation. The other waiting methods are similar, so I will not analyze them one by one. Interested friends can take a look for themselves. Well, then enter the source code analysis stage.

/**
 * await 是一个响应中断的等待方法,主要逻辑流程如下:
 * 1. 如果线程中断了,抛出 InterruptedException 异常
 * 2. 将线程封装到节点对象里,并将节点添加到条件队列尾部
 * 3. 保存并完全释放同步状态,保存下来的同步状态在重新竞争锁时会用到
 * 4. 线程进入等待状态,直到被通知或中断才会恢复运行
 * 5. 使用第3步保存的同步状态去竞争独占锁
 */
public final void await() throws InterruptedException {
    // 线程中断,则抛出中断异常,对应步骤1
    if (Thread.interrupted())
        throw new InterruptedException();
    
    // 添加等待节点到条件队列尾部,对应步骤2
    Node node = addConditionWaiter();
    
    // 保存并完全释放同步状态,对应步骤3。此方法的意义会在后面详细说明。
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    
    /*
     * 判断节点是否在同步队列上,如果不在则阻塞线程。
     * 循环结束的条件:
     * 1. 其他线程调用 singal/singalAll,node 将会被转移到同步队列上。node 对应线程将
     *    会在获取同步状态的过程中被唤醒,并走出 while 循环。
     * 2. 线程在阻塞过程中产生中断
     */ 
    while (!isOnSyncQueue(node)) {
        // 调用 LockSupport.park 阻塞当前线程,对应步骤4
        LockSupport.park(this);
        
        /*
         * 检测中断模式,这里有两种中断模式,如下:
         * THROW_IE:
         *     中断在 node 转移到同步队列“前”发生,需要当前线程自行将 node 转移到同步队
         *     列中,并在随后抛出 InterruptedException 异常。
         *     
         * REINTERRUPT:
         *     中断在 node 转移到同步队列“期间”或“之后”发生,此时表明有线程正在调用 
         *     singal/singalAll 转移节点。在该种中断模式下,再次设置线程的中断状态。
         *     向后传递中断标志,由后续代码去处理中断。
         */
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    
    /*
     * 被转移到同步队列的节点 node 将在 acquireQueued 方法中重新获取同步状态,注意这里
     * 的这里的 savedState 是上面调用 fullyRelease 所返回的值,与此对应,可以把这里的 
     * acquireQueued 作用理解为 fullyAcquire(并不存在这个方法)。
     * 
     * 如果上面的 while 循环没有产生中断,则 interruptMode = 0。但 acquireQueued 方法
     * 可能会产生中断,产生中断时返回 true。这里仍将 interruptMode 设为 REINTERRUPT,
     * 目的是继续向后传递中断,acquireQueued 不会处理中断。
     */
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    
    /*
     * 正常通过 singal/singalAll 转移节点到同步队列时,nextWaiter 引用会被置空。
     * 若发生线程产生中断(THROW_IE)或 fullyRelease 方法出现错误等异常情况,
     * 该引用则不会被置空
     */ 
    if (node.nextWaiter != null) // clean up if cancelled
        // 清理等待状态非 CONDITION 的节点
        unlinkCancelledWaiters();
        
    if (interruptMode != 0)
        /*
         * 根据 interruptMode 觉得中断的处理方式:
         *   THROW_IE:抛出 InterruptedException 异常
         *   REINTERRUPT:重新设置线程中断标志
         */ 
        reportInterruptAfterWait(interruptMode);
}

/** 将当先线程封装成节点,并将节点添加到条件队列尾部 */
private Node addConditionWaiter() {
    Node t = lastWaiter;
    /*
     * 清理等待状态为 CANCELLED 的节点。fullyRelease 内部调用 release 发生异常或释放同步状
     * 态失败时,节点的等待状态会被设置为 CANCELLED。所以这里要清理一下已取消的节点
     */
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    
    // 创建节点,并将节点置于队列尾部
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

/** 清理等待状态为 CANCELLED 的节点 */ 
private void unlinkCancelledWaiters() {
    Node t = firstWaiter;
    // 指向上一个等待状态为非 CANCELLED 的节点
    Node trail = null;
    while (t != null) {
        Node next = t.nextWaiter;
        if (t.waitStatus != Node.CONDITION) {
            t.nextWaiter = null;
            /*
             * trail 为 null,表明 next 之前的节点等待状态均为 CANCELLED,此时更新 
             * firstWaiter 引用的指向。
             * trail 不为 null,表明 next 之前有节点的等待状态为 CONDITION,这时将 
             * trail.nextWaiter 指向 next 节点。
             */
            if (trail == null)
                firstWaiter = next;
            else
                trail.nextWaiter = next;
            // next 为 null,表明遍历到条件队列尾部了,此时将 lastWaiter 指向 trail
            if (next == null)
                lastWaiter = trail;
        }
        else
            // t.waitStatus = Node.CONDITION,则将 trail 指向 t
            trail = t;
        t = next;
    }
}
   
/**
 * 这个方法用于完全释放同步状态。这里解释一下完全释放的原因:为了避免死锁的产生,锁的实现上
 * 一般应该支持重入功能。对应的场景就是一个线程在不释放锁的情况下可以多次调用同一把锁的 
 * lock 方法进行加锁,且不会加锁失败,如失败必然导致导致死锁。锁的实现类可通过 AQS 中的整型成员
 * 变量 state 记录加锁次数,每次加锁,将 state++。每次 unlock 方法释放锁时,则将 state--,
 * 直至 state = 0,线程完全释放锁。用这种方式即可实现了锁的重入功能。
 */
final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        // 获取同步状态数值
        int savedState = getState();
        // 调用 release 释放指定数量的同步状态
        if (release(savedState)) {
            failed = false;
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        // 如果 relase 出现异常或释放同步状态失败,此处将 node 的等待状态设为 CANCELLED
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}

/** 该方法用于判断节点 node 是否在同步队列上 */
final boolean isOnSyncQueue(Node node) {
    /*
     * 节点在同步队列上时,其状态可能为 0、SIGNAL、PROPAGATE 和 CANCELLED 其中之一,
     * 但不会为 CONDITION,所以可已通过节点的等待状态来判断节点所处的队列。
     * 
     * node.prev 仅会在节点获取同步状态后,调用 setHead 方法将自己设为头结点时被置为 
     * null,所以只要节点在同步队列上,node.prev 一定不会为 null
     */
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
        
    /*
     * 如果节点后继被为 null,则表明节点在同步队列上。因为条件队列使用的是 nextWaiter 指
     * 向后继节点的,条件队列上节点的 next 指针均为 null。但仅以 node.next != null 条
     * 件断定节点在同步队列是不充分的。节点在入队过程中,是先设置 node.prev,后设置 
     * node.next。如果设置完 node.prev 后,线程被切换了,此时 node.next 仍然为 
     * null,但此时 node 确实已经在同步队列上了,所以这里还需要进行后续的判断。
     */
    if (node.next != null)
        return true;
        
    // 在同步队列上,从后向前查找 node 节点
    return findNodeFromTail(node);
}

/** 由于同步队列上的的节点 prev 引用不会为空,所以这里从后向前查找 node 节点 */
private boolean findNodeFromTail(Node node) {
    Node t = tail;
    for (;;) {
        if (t == node)
            return true;
        if (t == null)
            return false;
        t = t.prev;
    }
}

/** 检测线程在等待期间是否发生了中断 */
private int checkInterruptWhileWaiting(Node node) {
    return Thread.interrupted() ?
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
        0;
}

/** 
 * 判断中断发生的时机,分为两种:
 * 1. 中断在节点被转移到同步队列前发生,此时返回 true
 * 2. 中断在节点被转移到同步队列期间或之后发生,此时返回 false
 */
final boolean transferAfterCancelledWait(Node node) {

    // 中断在节点被转移到同步队列前发生,此时自行将节点转移到同步队列上,并返回 true
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
        // 调用 enq 将节点转移到同步队列中
        enq(node);
        return true;
    }
    
    /*
     * 如果上面的条件分支失败了,则表明已经有线程在调用 signal/signalAll 方法了,这两个
     * 方法会先将节点等待状态由 CONDITION 设置为 0 后,再调用 enq 方法转移节点。下面判断节
     * 点是否已经在同步队列上的原因是,signal/signalAll 方法可能仅设置了等待状态,还没
     * 来得及转移节点就被切换走了。所以这里用自旋的方式判断 signal/signalAll 是否已经完
     * 成了转移操作。这种情况表明了中断发生在节点被转移到同步队列期间。
     */
    while (!isOnSyncQueue(node))
        Thread.yield();
    }
    
    // 中断在节点被转移到同步队列期间或之后发生,返回 false
    return false;
}

/**
 * 根据中断类型做出相应的处理:
 * THROW_IE:抛出 InterruptedException 异常
 * REINTERRUPT:重新设置中断标志,向后传递中断
 */
private void reportInterruptAfterWait(int interruptMode)
    throws InterruptedException {
    if (interruptMode == THROW_IE)
        throw new InterruptedException();
    else if (interruptMode == REINTERRUPT)
        selfInterrupt();
}

/** 中断线程 */   
static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

3.2 Notification

/** 将条件队列中的头结点转移到同步队列中 */
public final void signal() {
    // 检查线程是否获取了独占锁,未获取独占锁调用 signal 方法是不允许的
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    
    Node first = firstWaiter;
    if (first != null)
        // 将头结点转移到同步队列中
        doSignal(first);
}
    
private void doSignal(Node first) {
    do {
        /*
         * 将 firstWaiter 指向 first 节点的 nextWaiter 节点,while 循环将会用到更新后的 
         * firstWaiter 作为判断条件。
         */ 
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        // 将头结点从条件队列中移除
        first.nextWaiter = null;
    
    /*
     * 调用 transferForSignal 将节点转移到同步队列中,如果失败,且 firstWaiter
     * 不为 null,则再次进行尝试。transferForSignal 成功了,while 循环就结束了。
     */
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

/** 这个方法用于将条件队列中的节点转移到同步队列中 */
final boolean transferForSignal(Node node) {
    /*
     * 如果将节点的等待状态由 CONDITION 设为 0 失败,则表明节点被取消。
     * 因为 transferForSignal 中不存在线程竞争的问题,所以下面的 CAS 
     * 失败的唯一原因是节点的等待状态为 CANCELLED。
     */ 
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    // 调用 enq 方法将 node 转移到同步队列中,并返回 node 的前驱节点 p
    Node p = enq(node);
    int ws = p.waitStatus;
    
    /*
     * 如果前驱节点的等待状态 ws > 0,则表明前驱节点处于取消状态,此时应唤醒 node 对应的
     * 线程去获取同步状态。如果 ws <= 0,这里通过 CAS 将节点 p 的等待设为 SIGNAL。
     * 这样,节点 p 在释放同步状态后,才会唤醒后继节点 node。如果 CAS 设置失败,则应立即
     * 唤醒 node 节点对应的线程。以免因 node 没有被唤醒导致同步队列挂掉。关于同步队列的相关的
     * 知识,请参考我的另一篇文章“AbstractQueuedSynchronizer 原理分析 - 独占/共享模式”,
     * 链接为:http://t.cn/RuERpHl
     */
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

After reading the analysis of the signal method, let's take a look at the source code analysis of signalAll, as follows:

public final void signalAll() {
    // 检查线程是否获取了独占锁
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignalAll(first);
}

private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    /*
     * 将条件队列中所有的节点转移到同步队列中。与 doSignal 方法略有不同,主要区别在 
     * while 循环的循环条件上,下的循环只有在条件队列中没节点后才终止。
     */ 
    do {
        Node next = first.nextWaiter;
        // 将 first 节点从条件队列中移除
        first.nextWaiter = null;
        // 转移节点到同步队列上
        transferForSignal(first);
        first = next;    
    } while (first != null);
}

4. Other

When I read the source code of ConditionObject, I found a problem - the await method does not do synchronization control. At the beginning of the signal and signalAll methods, isHeldExclusively will be called to check whether the thread has acquired the exclusive lock. If the exclusive lock is not acquired, the caller will throw an exception. But in the await method, there is no relevant detection. There is no problem if calling the await method in the correct way of using it means calling the await method when the lock is acquired. But if the method is called without acquiring the lock, there will be thread competition, which will destroy the structure of the condition queue. Here's a look at the source code of the method for adding a new node, as follows:

private Node addConditionWaiter() {
    Node t = lastWaiter;
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    Node node = new Node(Thread.currentThread(), Node.CONDITION);

    // 存在竞争时将会导致节点入队出错
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

If there are threads t1 and t2 now, corresponding to nodes node1 and node2. Thread t1 acquires the lock, but t2 does not acquire the lock, and the condition queue is empty at this time, that is, firstWaiter = lastWaiter = null. Deduce the scenario that will cause the conditional queue to be destroyed, as follows:

  1. Time 1: Threads t1 and t2 are executed at the same time if (t == null), and both threads believe that the if condition is satisfied
  2. Time 2: Thread t1 initializes firstWaiter, that is, firstWaiter points to node1
  3. Time 3: Thread t2 modifies the point of firstWaiter again, and firstWaiter points to node2 at this time

As above, if the threads are executed in the above order, this will cause the queue to be destroyed. firstWaiter should point to node1, but instead it points to node2, and node1 is dequeued. What problems can this cause? This may cause thread t1 to block forever. Because signal/signalAll transfers nodes from the head of the condition queue, but node1 is not in the queue, node1 cannot be transferred to the synchronization queue. In the case of no interruption, the thread t1 corresponding to node1 will be permanently blocked.

There is no synchronization control for the await method, which leads to problems with the condition queue, which should be regarded as a defect in the implementation of ConditionObject. Regarding this defect, the blogger living in a dream also mentioned it in his article AbstractQueuedSynchronizer Source Code Interpretation--Condition of the sequel . And a bug was raised to the JDK developers. The BUG link is JDK-8187408 . Interested students can go and have a look.

5. Summary

At this point, the principle of Condition is analyzed. After analyzing the Condition principle, the analysis of AbstractQueuedSynchronizer is over. Overall, analyzing and blogging about AQS has given me a better understanding of how AQS works. AQS is the basis for the implementation of locks and other concurrent components in the JDK. Understanding the principles of AQS is of great benefit to the subsequent analysis of various locks and other synchronization components.

The implementation of AQS itself is more complicated, and it has to deal with various situations. As a class library, AQS has to consider and handle various possible situations, which is very complicated to implement. Not only that, AQS also well encapsulates the management of synchronization queues, thread blocking and wake-up and other basic operations, which greatly reduces the complexity of inherited classes to implement synchronization control functions. So, at the end of this article, once again pay tribute to the author of AQS, the Java guru Doug Lea.

Well, this is the end of this article, thank you for reading.

refer to

This article is released under the Creative Commons License 4.0, and the reprint must indicate the source in an obvious place
Author: coolblog
This article is simultaneously published on my personal blog: http://www.coolblog.xyz/?r=cb

cc
This work is licensed under the Creative Commons Attribution-NonCommercial-No Derivatives 4.0 International License.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325620516&siteId=291194637