开发者社区> 快乐崇拜007> 正文

jdk11源码--ReentrantLock之Condition源码分析

简介: jdk11 ReentrantLock之Condition源码分析
+关注继续查看

概述

jdk11源码-ReentrantLock源码一文中分析了ReentrantLock源码。里面有讲述在多个线程加入队列时的AQS内部状态:
在这里插入图片描述
==简单来说:condition的await和signal操作就是将node节点在这两个队列中转移的过程,这里重点关注waitstatus和nextwaiter两个字段。后面会逐行代码分析==
在这里插入图片描述

创建Condition

一个ReentrantLock可以创建多个Condition
Condition condition = lock.newCondition();
实际是创建一个ConditionObject对象,ConditionObject的定义在AbstractQueuedSynchronizer中。

nextWaiter

在之前的文章中介绍了,一个node对象中有两个重要的对象属性:

volatile int waitStatus;
Node nextWaiter;

waitStatus已经在jdk11源码-ReentrantLock源码一文中讲述。这里着重说一下nextWaiter的含义。
nextWaiter总共有三种类型的值:

  • Node.SHARED 共享模式
  • Node.EXCLUSIVE 独占模式
  • condition队列中下一个等待节点

condition queues condition队列仅在独占模式有效,使用一个简单的链接队列来保存因condition而等待的节点(线程)。这些节点可以被转移到AQS队列中重新获取锁。

awit

condition.await();使当前线程阻塞

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();//将waiter加入到condition等待队列(condition queue)
    int savedState = fullyRelease(node);//将当前线程占用的state锁资源全部释放。目的是为了将该线程从AQS队列中移除。
    int interruptMode = 0;
    //isOnSyncQueue:在AQS队列中返回true,否则返回false
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);//执行这里,说明当期线程不在AQS队列中,则需要被park挂起。

        //这里是被唤醒后的执行逻辑
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)//检查在park期间,是否被中断,根据具体情况来决定抛异常还是继续中断
            break;
    }
    
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // 清除被取消的节点
        unlinkCancelledWaiters();
    if (interruptMode != 0)//不等于0,表示park期间被中断过
        //当前线程被唤醒后,通过interruptMode来决定是继续中断还是抛异常
        reportInterruptAfterWait(interruptMode);
}

//将waiter加入到等待队列
private Node addConditionWaiter() {
    if(!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node t = lastWaiter;
    // 如果 lastWaiter 是取消状态(waitStatus != Node.CONDITION),那么将其清除
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();//删除condition队列中所有被取消的节点
        t = lastWaiter;
    }

    //新建一个waitStatus是Node.CONDITION的节点,表示当前节点(线程)的等待状态是condition。
    Node node = new Node(Node.CONDITION);
    
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

addConditionWaiter方法是将当前线程加入到condition的等待队列,lastWaiter 指向当前最新加入的node。

final int fullyRelease(Node node) {
    try {
        int savedState = getState();
        if (release(savedState))
            return savedState;
        throw new IllegalMonitorStateException();
    } catch (Throwable t) {
        node.waitStatus = Node.CANCELLED;
        throw t;
    }
}

fullyRelease方法有必要看一下,他首先获取state的值savedState,然后执行release(savedState),release方法之前已经分析过了,他会释放savedState数量的资源。这里也就是将当前线程锁定的资源全部释放,即当前线程释放独占锁。
也比较容易理解,condition只与独占锁配合使用,一个线程获取锁,但是被condition await以后挂起,肯定需要释放锁,其他线程才有机会获取锁而继续执行。

==根据前面的分析,condition的await会将线程node添加到condition队列,然后从AQS队列移除。在signal时,会将该线程node添加到AQS阻塞队列中。有关signal后面会分析。==
在释放了独占锁以后,就通过isOnSyncQueue方法循环检查当前线程(node)是否在AQS队列中,如果已经从AQS队列中移除,那么就可以放心的将其park了。

在park期间,线程有可能被中断或者unpark唤醒。那么就要判断接下来是需要继续park还是抛异常还是去重新获取锁。

final boolean isOnSyncQueue(Node node) {
    //如果当前线程node的状态是CONDITION或者node.prev为null时说明已经在Condition队列中了,所以返回false;
    //如果node添加到AQS阻塞队列中,那么他的waitstats会被初始化为0,或者被修改为-1,-3,肯定不是condition(-2)
    //如果node添加到AQS阻塞队列中,那么他的prev肯定不为空,至少也是head节点
    if (node.waitStatus == Node.CONDITION || node.prev == null) return false;
    //如果node有后继节点,那么他肯定在队列中。因为前面分析了,condition队列是不会设置next字段值的
    if (node.next != null) return true;
    
    /*
    * 执行到这里,说明node的waitStatus 不是CONDITION ,prev肯定也不是null。并且next肯定为null。
    * 第一次看这个代码,肯定会蒙圈,怎么可能会出现这种不一致的情况呢
    * 这种情况是因为在将node添加到AQS阻塞队列时,采用的CAS策略。CAS就有可能失败,所以会出现这种临时的不一致行为。
    * 在下面分析signal时会看到,signal会调用AbstractQueuedSynchronizer#enq方法,这个方法会先设置prev,然后再CAS设置tail和next。
    */
    return findNodeFromTail(node);
}

//新添加的节点都是加在队尾,所以从后向前找效率更高
private boolean findNodeFromTail(Node node) {
    for (Node p = tail;;) {
        if (p == node) return true;
        if (p == null) return false;
        p = p.prev;
    }
}

大家可以看到在AQS分析过程中个,大部分遍历循环查找都是从tail开始向前查找的。这是因为新加入的节点都加在队尾,从后往前找效率更高。

await方法中包含被唤醒后的执行逻辑,这个在分析为signal以后再看。

signal

public final void signal() {
    //isHeldExclusively()就一句话,判断互斥锁是不是当前线程加的。getExclusiveOwnerThread() == Thread.currentThread()
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)//唤醒condition队列中的第一个等待节点
        doSignal(first);
}

//从condition队列头往后找到一个没有被取消的节点,对其进行唤醒操作。
private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) && (first = firstWaiter) != null);
    //上面while条件表示:移除失败,并且condition队列还有后继节点
}

//将node节点从condition队列移动到AQS阻塞队列,成功返回true。
final boolean transferForSignal(Node node) {
    //设置失败,说明node被取消了。返回false,那么在上层循环中会继续查找下一个节点
    //设置成功,则node的waitstatus=0,再加上上面他的nextWaiter 被设置为null,也就是从condition队列中移除了。
    if (!node.compareAndSetWaitStatus(Node.CONDITION, 0))
        return false;

    //将node以CAS方式添加到AQS队尾
    //注意这里返回的p是老的队尾,也就是新加入node的前驱节点
    Node p = enq(node);
    int ws = p.waitStatus;
    //ws > 0说明前驱节点被取消;
    //ws<=0,那么需要将新加入节点的前驱节点waitstatus设置为Node.SIGNAL
    if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);//唤醒
    return true;
}
  • doSignal方法中,第一次循环时,first执行condition队列的头结点,firstWaiter 指向第二个节点。如果后面没有节点了,那么将lastWaiter 设置为null。然后将first的nextWaiter 设置为null,因为他马上就从condition队列中移除了。
  • 当移除失败,并且condition队列还有后继节点时会进行循环查找下一个没有被取消的节点进行唤醒。
  • 通过代码分析,从condition队列中移除的操作分两步:一个是将node节点的nextWaiter设置为null,第二是将他的waitStatus设置为初始值0。

一句话总结唤醒操作signal的工作:将线程node从condition队列转移到AQS队列中。

signal唤醒后的操作

从代码中可以看到,其实signal只是将node从condition队列移动到AQS队列,并没有主动调用LockSupport.unpark方法,还是依赖于AQS自身的机制真正unpark。

先看一下有哪些情况会让线程停止park继续往下执行:

  1. 另一个线程调用了signal方法,将node节点从condition队列转移到AQS阻塞队列,然后获取了锁(unpark)
  2. 另外一个线程对这个线程进行了中断
  3. 上面signal中的transferForSignal方法中有提到:前驱节点被取消 或者 修改AQS队列的前驱节点waitstatus设置为Node.SIGNAL时失败,这两种情况会调用unpark。

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

/*
* 如果中断了,检查中断是发送在signalled之前还是之后,
* 如果中断发生在signalled之前,返回THROW_IE
* 如果中断发生在signalled之后,返回REINTERRUPT
* 没有中断返回0
* 
* 注意:这里如果中断了,会首先会尝试将节点转移到AQS阻塞队列
*/
private int checkInterruptWhileWaiting(Node node) {
   return Thread.interrupted() ?
       (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
       0;
}

/*
* 该方法只有线程中断才会执行
* 尝试将节点转移到AQS队列。如果在signalled前取消了线程,返回true,上层checkInterruptWhileWaiting方法会抛出异常标识
* 
*/
final boolean transferAfterCancelledWait(Node node) {
    //如果这里CAS成功,说明node节点当前的状态时CONDITION,也就是说中断操作是在signalled前发生的。
    //因为:1、只有中断才会调用这个方法。2、上面doSignal方法中会在signal过程中将其状态由CONDITION修改为0。
   if (node.compareAndSetWaitStatus(Node.CONDITION, 0)) {
       enq(node);//将node节点加在AQS阻塞队列的队尾
       return true;
   }

    /*
    * 执行到这里说明上面CAS失败,说明中断发生在signalled之后。
    * signal方法中会将node以CAS方式添加到AQS队尾,执行enq方法,但是走到这里时enq方法可能还没有执行完,也就是node节点还没有完全加入到AQS队列中,所以这里自旋等待其完成。
    */
   while (!isOnSyncQueue(node))
       Thread.yield();
   return false;
}

上面将node从condition队列转移到AQS阻塞队列中以后,就开始尝试获取锁【==注意:无论是否中断,都会将其转移到AQS阻塞队列==】。

if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
    interruptMode = REINTERRUPT;//获取锁过程中被中断过,并且interruptMode 不是抛异常,那么重新设置中断标识。

acquireQueued方法用于获取锁资源。由于在await的时候,释放了savedState数量的资源,所以这里需要获取savedState数量的资源。acquireQueued返回结果是是否中断过,这里的中断是指在获取锁资源的过程中是否被中断过,与interruptMode 无关。
获取锁过程中被中断过,并且interruptMode 不是抛异常,那么重新设置中断标识。

接下来执行

if (node.nextWaiter != null) 
    unlinkCancelledWaiters();//清理condition队列中的被清除的节点

注意这里是清理的condition队列中的被取消的节点。
那么什么时候node.nextWaiter 不为空呢?在doSignal方法中会执行first.nextWaiter = null;一行代码,正常情况下来说,nextWaiter 应该都为空才对。
注意这里只是正常情况,别忘了被中断的情况,如果在signal之前就被中断了,执行了上面的acquireQueued方法,转移到了AQS队列中,此时该node节点的nextWaiter 不为空。但是他的waitstatus已经被设置为0.
在这里插入图片描述
unlinkCancelledWaiters比较简单,就是遍历condition队列,清楚被取消(waitStatus != Node.CONDITION))的节点。因为condition队列中的所有节点的waitStatus 肯定都是Node.CONDITION。

private void unlinkCancelledWaiters() {
    Node t = firstWaiter;
    Node trail = null;
    while (t != null) {
        Node next = t.nextWaiter;
        if (t.waitStatus != Node.CONDITION) {//waitStatus != Node.CONDITION)  就是被取消的节点
            t.nextWaiter = null;
            if (trail == null)
                firstWaiter = next;
            else
                trail.nextWaiter = next;
            if (next == null)
                lastWaiter = trail;
        }
        else
            trail = t;
        t = next;
    }
}

最后一步就是根据checkInterruptWhileWaiting方法返回的interruptMode中断标记,来决定抛异常还是给当前线程设置中断标记

private void reportInterruptAfterWait(int interruptMode)
    throws InterruptedException {
    if (interruptMode == THROW_IE)
        throw new InterruptedException();
    else if (interruptMode == REINTERRUPT)
        selfInterrupt();
}

好了 ,condition分析完毕,还是有点复杂度的。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
OPC Client 核心源码
好像技术一沾上工业,便有了很高的价值,大家三缄其口,谁都不点破这层窗户纸,好多的思路和源码都要从国外网站获得,国内总是有条件,有限制--就是不告诉你,怕教会徒弟,饿死师父吧。
769 0
HashMap源码分析(基于JDK1.6)
在Java集合类中最常用的除了ArrayList外,就是HashMap了。本文尽自己所能,尽量详细的解释HashMap的源码。一山还有一山高,有不足之处请之处,定感谢指定并及时修正。     在看HashMap源码之前先复习一下数据结构。
508 0
TreeMap源码分析——深入分析(基于JDK1.6)
TreeMap有Values、EntrySet、KeySet、PrivateEntryIterator、EntryIterator、ValueIterator、KeyIterator、DescendingKeyIterator、NavigableSubMap、AscendingSubMap、DescendingSubMap、SubMap、Entry共十三个内部类。
457 0
Java 读写锁 ReentrantReadWriteLock 源码分析
本文内容:读写锁 ReentrantReadWriteLock 的源码分析,基于 Java7/Java8。 阅读建议:虽然我这里会介绍一些 AQS 的知识,不过如果你完全不了解 AQS,看本文就有点吃力了。
899 0
LinkedHashMap源码分析(基于JDK1.6)
LinkedHashMap类似于HashMap,但是迭代遍历它时,取得“键值对”的顺序是插入次序,或者是最近最少使用(LRU)的次序。只比HashMap慢一点;而在迭代访问时反而更快,因为它使用链表维护内部次序(HashMap是基于散列表实现的,相关HashMap的内容可以看《Java集合类》和《HashMap源码分析》)。
605 0
+关注
快乐崇拜007
我是一名程序员
18
文章
0
问答
来源圈子
更多
+ 订阅
文章排行榜
最热
最新
相关电子书
更多
OceanBase 入门到实战教程
立即下载
阿里云图数据库GDB,加速开启“图智”未来.ppt
立即下载
实时数仓Hologres技术实战一本通2.0版(下)
立即下载