详解Condition的await和signal等待/通知机制

1,298 阅读3分钟

概述

Condition虽然在工作中没有机会用上, 所以也没太关注,最近开始看JDK源码的时候发现里面用了大量的condition,所以来研究一下内部实现原理.


Condition是AQS的一个内部类,是一个有头尾指针的单项链表 通过维护一个 等待队列 和 AQS的 同步队列 之间的交互达到等待唤醒的机制。 整体的图其实就是这么简单 image.png


public static void main(String[] args) throws Exception{
    # 创建了一个同步队列
    ReentrantLock lock = new ReentrantLock();
    # 创建了一个等待队列
    Condition condition = lock.newCondition();
    new Thread(()->{
            try {
                lock.lock();
                System.out.println("开始阻塞");
                # 调用了await方法后就会把当前线程放入等待队列,并且唤醒同步队列中的Node
                condition.await();
                # 能执行下来说明自己已经被踢出等待队列,并且放入同步队列,并且又抢到了锁
                System.out.println("阻塞结束");
            }catch (Exception e){

            }
            finally {
                lock.unlock();
            }
    }).start();
    
    Thread.sleep(1000);
    new Thread(()->{
            try {
                lock.lock();
                System.out.println("拿到锁释放资源");
                # 找到等待队列中的头节点踢出并插入到同步队列中去,同步队列的Node需要等当前线程释放锁后争抢
                condition.signal();
                Thread.sleep(1000);
                System.out.println("释放结束");
            }catch (Exception e){

            } finally {
                lock.unlock();
            }
    }).start();
    System.in.read();
}

等待队列

在AQS的内部类 ConditionObject 就是一个等待队列,而且一个 ReentrantLock 可以创建多个等待队列。先来看看 ConditionObject源码,可以看到就是一个头尾指针的队列,没啥东西

public class ConditionObject implements Condition, java.io.Serializable {
    # 等待队列的头指针
    private transient Node firstWaiter;
    # 尾指针
    private transient Node lastWaiter;

    public ConditionObject() { }
   
}

await

await方法是把当前线程封装成一个 Node节点然后放入等待队列,我们看看实现逻辑

     public final void await() throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        # 创建一个节点并且插入了队列尾部
        Node node = addConditionWaiter();
        # 释放 AQS的锁
        int savedState = fullyRelease(node);
        int interruptMode = 0;
        # 判断是不是在 AQS的同步队列中,如果是就退出循环
        while (!isOnSyncQueue(node)) {
            # 当前线程陷入睡眠,等待唤醒
            LockSupport.park(this);
            if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                break;
        }
        # 唤醒之后自己已经在同步队列中了,走AQS获取锁的逻辑
        if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
            interruptMode = REINTERRUPT;
        if (node.nextWaiter != null) // clean up if cancelled
            unlinkCancelledWaiters();
        if (interruptMode != 0)
            reportInterruptAfterWait(interruptMode);
    }
     # 创建一个等待队列的节点并插入队列尾部
    private Node addConditionWaiter() {
        Node t = lastWaiter;
        // If lastWaiter is cancelled, clean out.
        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;
    }

signal

唤醒逻辑就是把等待队列的Node丢到同步队列中,很简单主要关注 transferForSignal 方法

    private void doSignal(Node first) {
        do {
            if ( (firstWaiter = first.nextWaiter) == null)
                lastWaiter = null;
            first.nextWaiter = null;
        } while (!transferForSignal(first) &&
                 (first = firstWaiter) != null);
    }
    public final void signal() {
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        Node first = firstWaiter;
        if (first != null)
            doSignal(first);
    }
    final boolean transferForSignal(Node node) {
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        # enq 就是加入 AQS的同步队列
        Node p = enq(node);
        int ws = p.waitStatus;
        # 下面的是优化速度逻辑,不用关注也可以
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

总结

至此,Condition大体逻辑就梳理清楚了, 调用 await方法释放锁,然后自己加入 等待队列, signal 方法让Node放入 同步队列 去争抢锁资源。