并发系列(十)-----AQS详解等待队列

本文深入探讨了AQS框架中的等待队列机制,详细解析了等待队列的源码,包括如何添加等待节点、释放资源、改变线程状态以及等待通知模型。同时,文章阐述了await和signal方法在等待队列中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一 简介

前面几篇已经将AQS的框架,AQS的同步队列,AQS中的独占模式下资源获取,AQS共享模式下的资源的获取总结了一下,接下来就剩最后一部分了,等待队列。等待队列又叫条件队列,它只能使用在独占模式下。在我们阅读等待队列的时候要知道的一点是,等待队列中的线程是不会直接获取资源state的,它也是通过同步队列来获取资源的。一会在看源码中会说明。

二 源码解析

我们知道同步队列和条件队列都共用了Node内部类作为了节点,其中同步队列中用了Node类中的prev和next。等待队列中使用了nextWaiter。AQS中的等待队列是由内部类ConditionObject维护是Condition的实现。子类中实现了await开头的方法,signal开头的方法,其中以await开头都是将线程设置成等待状态,而signal使用来唤醒被被等待的线程。我们看子类中是是如何为维护队列以及是实现父类中的功能的。下面是ConditionObject的维护队列的成员变量。


        /**
         * 等待对列的首节点
         */
        private transient Node firstWaiter;
        /**
         * 等待对列的尾节点
         */
        private transient Node lastWaiter;

这俩个成员变量是用来维护等待队列的。

ConditionObject的主要成员介绍完了,下面我们看看await方法是如何将线程至于等待状态的。下面是源码

/**
         *将线程添加到等待对列中,并设置线程状态
         *
         * @throws InterruptedException 中断异常
         */
        @Override
        public void await() throws InterruptedException {
            if (Thread.interrupted()) {
                throw new InterruptedException();
            }
            //将当前线程封装并添加到尾部
            Node node = addConditionWaiter();
            //调用线程释放线程所占有的资源并返回持有的资源
            int savedState = fullyRelease(node);
            //中断标志
            int interruptMode = 0;
            //当前线程的节点不在队列中的话
            while (!isOnSyncQueue(node)) {
                //将当前线程至于waiting状态等待唤醒或中断
                LockSupport.park(this);
                //线程中断时循环结束此时
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) {
                    break;
                }
            }
            //这里调用acquireQueued来获取资源了,走这里说明线程被添加到同步队列中去了或者是
            //线程中断了
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE) {
                interruptMode = REINTERRUPT;
            }
            //下一个节点部位null
            if (node.nextWaiter != null) {
                unlinkCancelledWaiter();
            }
            //不为0表示中断
            if (interruptMode != 0) {
                reportInterruptAfterWait(interruptMode);
            }

        }

在上面await方法中我们看到了添加节点和释放资源,为什么要释放资源呢?一会在看释放资源的源码是再说。现在来看等待队列是如何添加节点源码如下


        /**
         * 向等待对列中添加一个等待节点
         *
         * @return 返回添加的这个节点
         */
        private Node addConditionWaiter() {
            //获取到尾节点
            Node t = lastWaiter;
            if (t != null && t.waitStatus != Node.CONDITION) {
                //如果为节点不为空且尾节点的状态为等待状态
                //检查节点,并去除节点状态不为等待状态的节点
                unlinkCancelledWaiter();
                //重新获取尾节点经过上个方法尾节点可能发生变化
                t = lastWaiter;
            }
            //将当前线程封装节点并设置为等待状态
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null) {
                //如果头节点为空说明当前等待对列为空将将头节点初始化为封装好的节点同时也初始化了
                //等待对列
                firstWaiter = node;
            } else {
                //如果不为空将尾节点的nextWaiter设为封装好的节点
                t.nextWaiter = node;
            }
            lastWaiter = node;
            return node;
        }

这个方法比较好理解,添加等待节点,如果为等待队列为空的话初始化等待队列。其中有一个方法unlinkCancelledWaiter使用来检查队列中不为等待状态并移除的。

下面我们看释放资源,既然这个线程可以调用await方法那么说明当前线程在获取资源中,那么它就会持有一定数量的资源。只有将当前的资源释放了,同步节点的下一个线程才可以获取资源。如果不释放的话整个同步队列都会陷入等待中。下面是源码

    /**
     * 调用者释放当前线程资源
     *
     * @param node 当前线程的节点
     * @return 返回释放前的状态
     */
    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 {
            if (failed) {
                node.waitStatus = Node.CANCELLED;
            }
        }
    }

其中资源的释放在独占模式的时候就已经看过了,就是操作资源和唤醒下一个线程。注意在释放资源后,该节点就不再同步队列中了,所以当await方法在往下走时就会进入while循环中将线程改变至waiting状态中,跳出while循环有两种可能1.等待节点的添加到同步队列中2.执行break 如果要执行break就要满足if中的条件。下面是if中的条件的源码。

        /**
         * 检查中断
         *
         * @param node 节点
         * @return 检查中断 如果是没有中断返回0
         */
        private int checkInterruptWhileWaiting(Node node) {
            //中断的话返回THROW_IE 或REINTERRUPT 这两个值都不0
            return Thread.interrupted() ?
                    (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 0;
        }

中断的话就跳出了循环去获取资源调用acquireQueued(node, savedState)方法这个方法里面处理了中断。

跳出循环除了中断还有一种可能就是该节点添加到队列中,而且没有中断,那么可以猜测在signal方法中一定有将等待队列中的节点添加到同步队列中的工作要做。下面是源码

       /**
         * 当等待对列中的头节点移动到同步对列
         *
         * @param first 头节点
         */
        private void doSignal(Node first) {
            do {
                //添加失败的话,在将等待节点的下一个节点设置设为头节点
                if ((firstWaiter = first.nextWaiter) == null) {
                    lastWaiter = null;
                }
            } while (!transferForSignal(first) && (first = firstWaiter) != null);
            //添加成功退出循环
        }

上面方法中并没有有唤醒的操作,我们看while里面的方法transferForSignal(first),下面是源码

/**
     * 将等待对列中的节点添加到同步列
     *
     * @param node 节点
     * @return true 表示成功
     */
    final boolean transferForSignal(Node node) {
        //如果这个节点的状态不为等待状态,那么直接添加失败
        if (!compareAndSetWaitStatus(node, Node.CANCELLED, 0)) {
            return false;
        }
        //添加尾节点到同步对列中
        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) {
            //唤醒竞争资源,与await()方法中的 LockSupport.park(node.thread)对应
            LockSupport.unpark(node.thread);
        }
        return true;
    }

终于在这个方法中看到了唤醒,我们看到在唤醒前调用了enq()将等待队列中的节点添加到了同步节点,这样的话,await方法也就跳出了循环并且可以去获取资源了。获取资源的条件就是要等待调用signal方法,这就是等待队列的原理。我感觉就是一个等待通知模型,添加队列的过程。

三 总结

等待队列的主要方法就await,和signal

await完成的工作是添加等待节点,释放资源,改变线程状态到waiting等待唤醒.去竞争资源,竞争资源的条件是该节点添加到同步对列中

signal完成的工作是,添加等待节点的头节点到同步对列中,唤醒线程.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值