zoukankan      html  css  js  c++  java
  • 二十三、并发编程之深入解析Condition源码

    二十三、并发编程之深入解析Condition源码

    1、Object的wait和notify/notifyAll方法与Condition区别

    任何一个java对象都继承于Object类,在线程间实现通信的往往会应用到Object的几个方法,比如wait(),wait(long timeout),wait(long timeout, int nanos)与notify(),notifyAll()几个方法实现等待/通知机制。在java Lock体系下也有方法实现等待/通知机制。从整体上来看Object的wait/notify是与对象监视器配合完成线程间的等待/通知机制,而Condition是与Lock配合完成等待通知机制,前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。

    两者除了在使用方式上不同外,在功能特性上还是有很多的不同:

    • 1.Condition能够支持不响应中断,而Object的wait/notify不支持;
    • 2.Condition能够支持多个等待队列(new多个Condition对象),而Object的wait/notify只能支持一个;
    • 3.Condition能够支持超时时间的设置,而Object的wait/notify不支持

    2、参照Object的wait和notify/notifyAll方法,Condition也提供了同样的方法:

    1.针对Object的wait方法

    • 1.void await() throws InterruptedException:当前线程进入等待状态,如果其他线程调用condition的signal或者signalAll方法并且当前线程获取Lock从await方法返回,如果在等待状态中被中断会抛出被中断异常;
    • 2.long awaitNanos(long nanosTimeout):当前线程进入等待状态直到被通知,中断或者超时;
    • 3.boolean await(long time, TimeUnit unit)throws InterruptedException:同第二种,支持自定义时间单位
    • 4.boolean awaitUntil(Date deadline) throws InterruptedException:当前线程进入等待状态直到被通知,中断或者到了某个时间

    2.针对Object的notify/notifyAll方法

    • 1.void signal():唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回。
    • 2.void signalAll():与1的区别在于能够唤醒所有等待在condition上的线程

    二、Condition实现原理分析

    1、等待队列

    创建一个condition对象是通过lock.newCondition(),而这个方法实际上是会new出一个ConditionObject对象,该类是AQS(AbstractQueuedSynchronizer)的一个内部类。condition要和lock配合使用的,也就是condition和Lock是绑定在一起的,而lock的实现原理又依赖于AQS。在锁机制的实现上,AQS内部维护了一个同步队列,如果是独占式锁的话,所有获取锁失败的线程的尾插入到同步队列,同样的,condition内部也是使用同样的方式,内部维护了一个 等待队列,所有调用condition.await方法的线程会加入到等待队列中,并且线程状态转换为等待状态。另外注意到ConditionObject中有两个成员变量:

    private transient Node firstWaiter;//等待队列头指针
    private transient Node lastWaiter;//等待队列尾指针
    

    这样我们就可以看出来ConditionObject通过持有等待队列的头尾指针来管理等待队列。主要注意的是Node类复用了在AQS中的Node类,Node类有这样一个属性:

    //后继节点
    Node nextWaiter;

    进一步说明,等待队列是一个单向队列,而AQS同步队列是一个双向队列。

    接下来我们用一个demo,新建了10个线程,没有线程先获取锁,然后调用condition.await方法释放锁将当前线程加入到等待队列中,通过debug控制当走到第10个线程的时候查看firstWaiter即等待队列中的头结点。

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(() -> {
                lock.lock();
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            });
            thread.start();
        }
    }
    

    debug模式下情景图如下:
    在这里插入图片描述
    从这个图我们可以很清楚的看到这样几点:

    1. 调用condition.await方法后线程依次尾插入到等待队列中,如图队列中的线程引用依次为Thread-0,Thread-1,Thread-2…Thread-8;
    2. 等待队列是一个单向队列。通过我们的猜想然后进行实验验证,我们可以得出等待队列的示意图如下图所示:

    在这里插入图片描述
    同时还有一点需要注意的是:我们可以多次调用lock.newCondition()方法创建多个condition对象,也就是一个lock可以持有多个等待队列。而在之前利用Object的方式实际上是指在对象Object对象监视器上只能拥有一个同步队列和一个等待队列,而并发包中的Lock拥有一个同步队列和多个等待队列。示意图如下:
    在这里插入图片描述
    如图所示,ConditionObject是AQS的内部类,因此每个ConditionObject能够访问到AQS提供的方法,相当于每个Condition都拥有所属同步器的引用。

    2、await实现原理

    当调用condition.await()方法后会使得当前获取lock的线程进入到等待队列,如果该线程能够从await()方法返回的话一定是该线程获取了与condition相关联的lock。

    1.await()方法源码

    public final void await() throws InterruptedException {
        if (Thread.interrupted())//线程中断抛出异常
            throw new InterruptedException();
        // 将当前线程包装成Node,尾插入到等待队列中
        Node node = addConditionWaiter();
        // 释放当前线程所占用的lock,在释放的过程中会唤醒同步队列中的下一个节点
        int savedState = fullyRelease(node);
        int interruptMode = 0;
        while (!isOnSyncQueue(node)) {//如果线程没有进入同步队列
            // 当前线程进入到等待状态
            LockSupport.park(this);
            if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                break;
        }
        // 自旋等待获取到同步状态(即获取到lock)
        if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
            interruptMode = REINTERRUPT;
        if (node.nextWaiter != null) // clean up if cancelled
            unlinkCancelledWaiters();
        //处理被中断的情况
        if (interruptMode != 0)
            reportInterruptAfterWait(interruptMode);
    }
    

    我们都知道当当前线程调用condition.await()方法后,会使得当前线程释放lock然后加入到等待队列中,直至被signal/signalAll后会使得当前线程从等待队列中移至到同步队列中去,直到获得了lock后才会从await方法返回,或者在等待时被中断会做中断处理。

    2.将当前线程添加到等待队列中去

    在第1步中调用addConditionWaiter将当前线程添加到等待队列中,该方法源码为:

    private Node addConditionWaiter() {
        Node t = lastWaiter;//尾节点
        // If lastWaiter is cancelled, clean out.
        if (t != null && t.waitStatus != Node.CONDITION) {
            unlinkCancelledWaiters();//清除等待节点
            t = lastWaiter;
        }
        //将当前线程包装成Node
        Node node = new Node(Thread.currentThread(), Node.CONDITION);
        if (t == null)//队列为空
            firstWaiter = node;//设置node为头节点
        else//队列不为空。从队列尾部插入
            //尾插入
            t.nextWaiter = node;
        //更新lastWaiter
        lastWaiter = node;
        return node;
    }
    

    将当前节点包装成Node,如果等待队列的firstWaiter为null的话(等待队列为空队列),则将firstWaiter指向当前的Node,否则,更新lastWaiter(尾节点)即可。就是通过尾插入的方式将当前线程封装的Node插入到等待队列中即可,同时可以看出等待队列是一个不带头结点的链式队列,学习AQS知道同步队列是一个带头结点的链式队列,这是两者的一个区别。

    3. 释放锁的过程

    将当前节点插入到等待对列之后,会使当前线程释放lock,由fullyRelease方法实现。
    fullyRelease源码为:

    final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            int savedState = getState();
            if (release(savedState)) {
                //成功释放同步状态
                failed = false;
                return savedState;
            } else {
                //不成功释放同步状态抛出异常
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

    调用AQS的模板方法release方法释放AQS的同步状态并且唤醒在同步队列中头结点的后继节点引用的线程,如果释放成功则正常返回,若失败的话就抛出异常。

    4.怎样从await方法退出?

    现在回过头再来看await方法有这样一段逻辑:

    while (!isOnSyncQueue(node)) {//如果线程没有进入同步队列
        //当前线程进入到等待状态
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    

    很显然,当线程第一次调用condition.await()方法时,会进入到这个while()循环中,然后通过LockSupport.park(this)方法使得当前线程进入等待状态,那么要想退出这个await方法第一个前提条件自然而然的是要先退出这个while循环,出口就只剩下两个地方:

    • 逻辑走到break退出while循环;
    • while循环中的逻辑判断为false。

    再看代码出现第1种情况的条件是当前等待的线程被中断后代码会走到break退出,第二种情况是当前节点被移动到了同步队列中(即另外线程调用的condition的signal或者signalAll方法),while中逻辑判断为false后结束while循环。
    总结下,就是当前线程被中断或者调用condition.signal/condition.signalAll方法当前节点移动到了同步队列后 ,这是当前线程退出await方法的前提条件。当退出while循环后就会调用acquireQueued(node, savedState),该方法的作用是在自旋过程中线程不断尝试获取同步状态,直至成功(线程获取到lock)。这样也说明了退出await方法必须是已经获得了condition引用(关联)的lock。

    5.await方法示意图如下图:

    在这里插入图片描述
    如图,调用condition.await方法的线程必须是已经获得了lock,也就是当前线程是同步队列中的头结点。调用该方法后会使得当前线程所封装的Node尾插入到等待队列中。

    5.超时机制的支持

    condition还额外支持了超时机制,使用者可调用方法awaitNanos,awaitUtil。这两个方法的实现原理,基本上与AQS中的tryAcquire方法如出一辙。

    6.不响应中断的支持

    要想不响应中断可以调用condition.awaitUninterruptibly()方法,该方法的源码为:

    public final void awaitUninterruptibly() {
        Node node = addConditionWaiter();//加入等待队列
        int savedState = fullyRelease(node);//释放线程所有锁
        boolean interrupted = false;
        while (!isOnSyncQueue(node)) {//如果线程没有进入同步队列
        	//当前线程进入等待状态
            LockSupport.park(this);
            if (Thread.interrupted())
                interrupted = true;
        }
        if (acquireQueued(node, savedState) || interrupted)
            selfInterrupt();
    }
    

    这段方法与上面的await方法基本一致,只不过减少了对中断的处理,并省略了reportInterruptAfterWait方法抛被中断的异常。

    3、signal/signalAll实现原理

    调用condition的signal或者signalAll方法可以将等待队列中等待时间最长的节点移动到同步队列中,使得该节点能够有机会获得lock。按照等待队列是先进先出(FIFO)的,所以等待队列的头节点必然会是等待时间最长的节点,也就是每次调用condition的signal方法是将头节点移动到同步队列中。

    1.signal方法源码为:

    public final void signal() {
        //先检测当前线程是否已经获取lock
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        //获取等待队列中第一个节点,之后的操作都是针对这个节点
        Node first = firstWaiter;
        if (first != null)
            doSignal(first);
    }
    

    signal方法首先会检测当前线程是否已经获取lock,如果没有获取lock会直接抛出异常,如果获取的话再得到等待队列的头指针引用的节点,之后的操作的doSignal方法也是基于该节点。

    2.doSignal方法源码为:

    private void doSignal(Node first) {
        do {
            if ( (firstWaiter = first.nextWaiter) == null)
                lastWaiter = null;
            //1. 将头结点从等待队列中移除
            first.nextWaiter = null;
            //2. while中transferForSignal方法对头结点做真正的处理
        } while (!transferForSignal(first) &&
                 (first = firstWaiter) != null);
    }

    3.transferForSignal 源码

    具体逻辑请看注释,真正对头节点做处理的逻辑在transferForSignal放,该方法源码为:

    final boolean transferForSignal(Node node) {
        //1. 更新状态为0
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
    
        //2.将该节点移入到同步队列中去
        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }
    

    关键逻辑请看注释,这段代码主要做了两件事情

    • 将头结点的状态更改为CONDITION;
    • 调用enq方法,将该节点尾插入到同步队列中,关于enq方法请看AQS的底层实现这篇文章。

    现在我们可以得出结论:调用condition的signal的前提条件是当前线程已经获取了lock,该方法会使得等待队列中的头节点即等待时间最长的那个节点移入到同步队列,而移入到同步队列后才有机会使得等待线程被唤醒,即从await方法中的LockSupport.park(this)方法中返回,从而才有机会使得调用await方法的线程成功退出。

    4.signal执行示意图如下图:

    在这里插入图片描述

    5.signalAll源码

    signalAll与signal方法的区别体现在doSignalAll方法上,前面我们已经知道doSignal方法只会对等待队列的头节点进行操作,而doSignalAll的源码为:

    private void doSignalAll(Node first) {
        lastWaiter = firstWaiter = null;
        do {
            Node next = first.nextWaiter;
            first.nextWaiter = null;
            transferForSignal(first);
            first = next;
        } while (first != null);
    }
    

    该方法只不过时间等待队列中的每一个节点都移入到同步队列中,即“通知”当前调用condition.await()方法的每一个线程。

    三、await与signal/signalAll的结合思考

    文章开篇提到等待/通知机制,通过使用condition提供的await和signal/signalAll方法就可以实现这种机制,await和signal和signalAll方法就像一个开关控制着线程A(等待方)和线程B(通知方)。它们之间的关系可以用下面一个图来表现得更加贴切:
    在这里插入图片描述
    如图,线程awaitThread先通过lock.lock()方法获取锁成功后调用了condition.await方法进入等待队列,而另一个线程signalThread通过lock.lock()方法获取锁成功后调用了condition.signal或者signalAll方法,使得线程awaitThread能够有机会移入到同步队列中,当其他线程释放lock后使得线程awaitThread能够有机会获取lock,从而使得线程awaitThread能够从await方法中退出执行后续操作。如果awaitThread获取lock失败会直接进入到同步队列。

  • 相关阅读:
    WINNER队成立(第二天)
    新学期组队合作
    构建之法第8、9、10章
    第一阶段冲刺(第一天)
    作业六:小学生四则运算之NABCD模型与产品Backlog。
    构建之法第六章、第七章观后感
    四则运算,测试与封装。
    使用Webpack4 搭建React项目
    算法一小时--希尔排序
    算法一小时-插入排序
  • 原文地址:https://www.cnblogs.com/kelelipeng/p/11643359.html
Copyright © 2011-2022 走看看