Java并发编程知识点总结(十二)——Condition中await和signal通知机制

1. Condition简介

之前在介绍AQS源码的时候,讲述了同步队列的独占模式和共享模式,排队都是在同步队列中。

但是实际上,AQS中还有条件队列。我们举个例子来解释条件队列的作用,例如我们排队去上厕所,通过排队最终获得了锁进入了厕所,但是不巧的是发现忘记带纸,遇到这种事情很无奈,但是也得接受这个事实,这时只能乖乖的出去准备好手纸(也就是进入了条件队列中等待),当然再出去之前还要把锁释放掉,好让后面排队的人进来,在准备好了手纸(条件满足)条件满足之后进入同步队列中去重新排队;

可能在平时我们比较少使用Condition队列,但是在一些并发容器的源码中经常会看到Condition的身影,例如:LinkedBlockingQueue, ArrayBlockingQueue等等。

Condition包含的两个方法是:await()和signal()。和Object.wait()、Object.notify()方法是相对应的。此外必须注意的是:执行await/signal方法,必须先获取锁,否则会抛出异常。这就和必须先执行synchronizer才能执行wait/notify方法一样。

2. 条件队列结构

以ReentrantLock为例,如果我们需要使用条件队列,就需要通过newCondition()方法创建Condition对象:

    public Condition newCondition() {
        return sync.newCondition();
    }

接着又会继续进入Sync类中的newCondition()方法:

    final ConditionObject newCondition() {
        return new ConditionObject();
    }

这个ConditionObject对象实际上是AQS的内部类,实现了Condition接口:

public class ConditionObject implements Condition, java.io.Serializable {

我们再进入Condition接口,看看具体包含哪些方法:

public interface Condition {
    // 响应中断的条件等待
    void await() throws InterruptedException;

    // 忽略中断的条件等待
    void awaitUninterruptibly();

    // 设置有限等待时间的条件等待(不支持自定义时间单位)
    long awaitNanos(long nanosTimeout) throws InterruptedException;

    // 设置有限等待时间的条件等待(支持自定义时间单位)
    boolean await(long time, TimeUnit unit) throws InterruptedException;

    // 设置绝对时间的有限等待
    boolean awaitUntil(Date deadline) throws InterruptedException;

    // 唤醒条件队列中的头节点
    void signal();

    // 唤醒条件队列中的所有节点
    void signalAll();
}

可以看到,方法其实都是await和signal方法,分别就是将当前线程进入条件队列和唤醒条件队列中的线程;

接下来回到ConditionObject类,其中有两个重要的成员变量:

    /** First node of condition queue. */
    private transient Node firstWaiter;
    /** Last node of condition queue. */
    private transient Node lastWaiter;

firstWaiter和lastWaiter分别指向等待队列中的第一个节点和最后一个节点。

此外,还有点需要注意的是,在这个Condition条件队列中,它是一个单向队列。和我们之前讲的AQS中的双向同步队列是不同的。我们通过一段代码来验证这个点:

    public static void main(String[] args) {
        final ReentrantLock lock = new ReentrantLock();
        final Condition condition = lock.newCondition();
        for(int i=0;i<10;i++){
            Thread thread = new Thread(){
                @Override
                public void run() {
                    lock.lock();
                    try {
                        condition.await();
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }finally {
                        lock.unlock();
                    }
                }
            };
            thread.start();  // 断点
        }
    }

在代码中,我们按照顺序创建了十个线程,并且都执行了condition.await()方法,然后debug停留在第十个线程的断点处:
在这里插入图片描述

从上图可以看到,虽然条件队列和同步队列中的节点都是AQS中的Node内部类,但是在条件队列中并没有使用prev和next指针,只使用了nextWaiter指向下一个节点,所以Condition条件队列是单向的。如下图:
在这里插入图片描述

此外,还有点要注意的是。一个Lock能够持有多个条件队列,也就是能够多次调用lock.newCondition()方法。如下图:
在这里插入图片描述

相比较,AQS中的同步队列就只能有一个。

3. await实现原理

首先我们进入conditionObject.await()方法中:

    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);
    }

从上面的代码中,我们可以看到首先是会将节点加入到条件队列中,进入到addConditionWaiter()方法中:

    private Node addConditionWaiter() {
        Node t = lastWaiter;   // 队尾节点
        // If lastWaiter is cancelled, clean out.
        if (t != null && t.waitStatus != Node.CONDITION) { // 队尾节点不是Condition
            unlinkCancelledWaiters();   // 消除队列中所有不是condition状态的节点
            t = lastWaiter;   // 获得新的队尾 
        }
        Node node = new Node(Thread.currentThread(), Node.CONDITION);  // 创建新节点
        if (t == null)  // 如果是空队列
            firstWaiter = node;  // 头节点指针指向node
        else
            t.nextWaiter = node;  // 否则队尾元素的next指向node
        lastWaiter = node;  // node成为新的尾节点
        return node;
    }

在addConditionWaiter()方法中,我们可以很清晰看到就是进行了一个简单的入队操作。其中会调用unlinkCancelledWaiters()消除队列中的非condition状态的节点,于是我们进入unlinkCancelledWaiters()方法:

    private void unlinkCancelledWaiters() {
        Node t = firstWaiter;  // 获得头指针指向的节点
        Node trail = null; // 指代上一次遍历的最后一个节点 
        while (t != null) {  // 遍历
            Node next = t.nextWaiter;  // 获得t的后继节点
            if (t.waitStatus != Node.CONDITION) { // 如果节点t不是condition状态
                t.nextWaiter = null;   // t节点出队
                if (trail == null)
                    firstWaiter = next;  // 更新头指针指向的节点
                else
                    trail.nextWaiter = next;
                if (next == null)   // 如果是最后一个节点了
                    lastWaiter = trail;  // 将trail节点成为新的尾指针指向的节点
            }
            else  // 如果t是condition状态
                trail = t;   
            t = next;   // 更新t节点
        }
    }

在unlinkCancelledWaiters()函数中就是从头节点遍历到尾节点,抛弃掉不是condition状态的节点,进行出队。

至此,addConditionWaiter()和unlinkCancelledWaiters()函数都介绍完了。回到await(),接下来会进入fullyRelease(node):

    final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            int savedState = getState();  // 获取state同步状态
            if (release(savedState)) {  // 释放锁
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();  // 如果当前锁不是当前线程持有,就报错
            }
        } finally {
            if (failed)  // 如果失败了,就cancelled
                node.waitStatus = Node.CANCELLED;
        }
    }

从fullyRelease函数中可以看到,这个函数是用来充分释放锁的,当然前提是你得先持有当前锁,否则就会报错。

再回到await()函数中,接下来就会判断while (!isOnSyncQueue(node)),第一次判断的时候肯定是为false的,会进入到while循环中。我们下面看看isOnSyncQueue(node)的代码:

    final boolean isOnSyncQueue(Node node) {
        // 判断当前状态是否为condition或者前驱节点为null
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        // 判断是否有后继节点
        if (node.next != null) // If has successor, it must be on queue
            return true;
        /*
         * node.prev can be non-null, but not yet on queue because
         * the CAS to place it on queue can fail. So we have to
         * traverse from tail to make sure it actually made it.  It
         * will always be near the tail in calls to this method, and
         * unless the CAS failed (which is unlikely), it will be
         * there, so we hardly ever traverse much.
         */
        return findNodeFromTail(node);  // 进行遍历查找
    }

在这个方法中,如果不符合前两个条件,就会进入findNodeFromTail(node)函数从后往前遍历,查看在同步队列中是否有这个节点:

    private boolean findNodeFromTail(Node node) {
        Node t = tail;  // 同步队列尾指针指向的位置
        for (;;) {  // 遍历
            if (t == node)
                return true;
            if (t == null)
                return false;
            t = t.prev;  // 不断寻找前驱
        }
    }

至此,经过上面的方法就能判断出当前节点是否在同步队列上了。

接着,我们回到await方法中,进入while(isOnSyncQueue(node))循环后,就会通过LockSupport阻塞。跳出循环就只有两种方式:解除阻塞进入到同步队列或遇到中断。

跳出循环后,这是当前节点已经在同步队列上面了,就会进入acquireQueued方法(AQS独占锁中已经介绍过)去排队尝试获取锁,获取锁成功后会接着调用unlinkCancelledWaiters()方法删除掉取消状态的节点。

至此,await()函数就结束了。方法的示意图如下:
在这里插入图片描述

4. signal实现原理

首先我们进入到conditionObject的signal()方法中:

    public final void signal() {
        if (!isHeldExclusively())   // 检测当前线程是否获得lock
            throw new IllegalMonitorStateException();
        Node first = firstWaiter;  // 获取condition队列的头节点
        if (first != null)
            doSignal(first);  // signal
    }

我们可以看到,首先是检查当前线程是否持有lock。然后接着会调用doSignal方法:

    private void doSignal(Node first) {
        do {
            if ( (firstWaiter = first.nextWaiter) == null)
                lastWaiter = null;
            first.nextWaiter = null; // 将头节点从等待队列中移除
        } while (!transferForSignal(first) && // 对头节点进行唤醒
                    (first = firstWaiter) != null);
    }

可以看到,首先是将头节点移出condition队列,然后是调用transferForSignal(first)方法对头节点唤醒:

    final boolean transferForSignal(Node node) {
        // CAS将condition状态切换成默认状态,如果失败就表明头节点是cacel状态
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        
        Node p = enq(node); // 对头节点进行同步队列入队操作
        int ws = p.waitStatus;  
        // 调用unpark方法进行唤醒
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

在transferForSignal(Node node)方法中,我们看到他是先将头节点加入到同步队列的末尾去排队,然后调用unpark方法的。这样就能够让await()函数里面调用park方法的线程解除阻塞,跳出循环。

至此signal()方法就介绍玩了,整体的示意图如下:
在这里插入图片描述

5. signalAll实现原理

signal和signalAll方法的区别在于,前者只会唤醒头节点的线程,而signalAll方法会唤醒condition条件队列里的所有线程。我们进入到signalAll()方法:

    public final void signalAll() {
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        Node first = firstWaiter;
        if (first != null)
            doSignalAll(first);  // 区别:调用doSignalAll方法
    }

这个方法的区别就是会调用doSignalAll()方法:

    private void doSignalAll(Node first) {
        lastWaiter = firstWaiter = null;
        do {
            Node next = first.nextWaiter;
            first.nextWaiter = null;
            transferForSignal(first);
            first = next;
        } while (first != null);  // 和doSignal方法的区别
    }

这个方法唯一的区别就是在while循环的条件部分,使得会循环不断唤醒条件队列中的线程。

6. 总结

在这里插入图片描述

我们总结下整个流程,图中的等待队列就是条件队列:

  1. 线程A处于同步队列中,竞争中获得锁
  2. 线程A在执行过程中调用Condition.await()方法,线程A释放锁,进入到condition条件队列中
  3. 当线程A成为条件队列头部时,并且其他线程调用signal方法时,就会进入到同步队列的尾部进行排队
  4. 线程A重新尝试获得锁

7. 例子

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionTest {
    private static ReentrantLock reentrantLock = new ReentrantLock();
    private static Condition condition = reentrantLock.newCondition();
    private static boolean flag = false;

    public static void main(String[] args) {
        Thread awaiter = new Thread(new Awaiter());
        Thread signaler = new Thread(new Signaler());
        awaiter.start();
        signaler.start();
    }

    static class Awaiter implements Runnable {

        @Override
        public void run() {
            reentrantLock.lock();
            try {
                while (!flag) {
                    System.out.println("Awaiter在等待资源");
                    condition.await();
                }
                System.out.println("Awaiter在获取资源成功");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                reentrantLock.unlock();
            }
        }
    }

    static class Signaler implements Runnable {

        @Override
        public void run() {
            reentrantLock.lock();
            try {
                flag = true;
                System.out.println("Signaler配置资源");
                condition.signal();
                System.out.println("Signaler唤醒队列");
            } finally {
                reentrantLock.unlock();
            }
        }
    }
}

输出结果:
在这里插入图片描述

代码中创建了两个线程,Awaiter和Signaler,Awaiter需要等待flag=true资源,执行了Await方法。Signaler则会将flag设置成true,并执行signal方法唤醒Awaiter。

参考文章:
详解Condition的await和signal等待/通知机制
AQS之条件队列Condition

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值