首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

并发工具类Condition介绍与源码解析

在之前介绍AQS源码的时候,还遗留了一个内部类ConditionObject没有介绍,它也是并发中至关重要的类。

主要作用

Condition主要提供多个await方法以及signal、signalAll方法,对标的是Object的wait、notify、notifyAll方法,对应的作用也一样。

ConditionObject是AQS的内部类并且实现了接口Condition,主要提供的功能也一样。

创建方法

只有ReentrantLock提供了获取AQS的Condition对象的方法。首先要new一个ReentrantLock对象,然后通过ReentrantLock的newCondition()方法获取Condition对象,每次获取都是创建一个新的Condition对象。

结构与主要方法

Condition(表示AQS中的ConditionObject,下同)只有两个属性firstWaiter、lastWaiter,类型是AQS的Node,也就是Condition也维护一个链表,firstWaiter、lastWaiter分别是链表的首位节点。

await方法源码分析

Condition提供了很多个await方法,主要区别在于是否在如果等待一定时间后不是否继续等待,这里分析下await最基础的无参方法,源代码流程图如下图:

简单分析下await方法:

首先创建一个Node(当前线程,状态-2)节点并加到等待链表的尾部,这个Node是AQS中的Node,也会保存当前线程;

第二步是释放锁,因为线程在等待过程中其他线程也需要使用lock,所以这里先释放锁,也就是修改AQS中state的值。

第三步是进入一个while循环,跳出循环判断的条件是节点状态不等于-2、并且要在同步队列中(在AQS维护的链表中),如果没在同步队列中则会进入循环挂起线程,当再次被唤醒的时候会优先判断线程是否中断,如果线程中断也会跳出循环,否则继续循环判断。

第四步是当当前线程跳出循环时,会重新设置锁,设置成功再去根据情况整理链表,如果线程是中断的,则根据情况抛出中断异常,或者中断。

从源码中可以得出,在调用await方法之前一定要先获取到锁,否则会抛出异常

signal方法:让线程跳出循环

知道了await方法的源码流程,就可以猜测signal方法的源码了,先来分析下await方法如何才能执行完,先不考虑中断线程的情况,要让线程跳出while循环需要两个条件:节点状态不能是-2、让节点在同步队列中;记住这里的同步队列是AQS维护的同步队列。

所以现在再来看signal方法的源码就简单了:

首先是拿到firstWaiter,如果为null则继续往链表后面找;

第二步是把firstWaiter的状态从等待状态改为0,如果修改失败则直接返回false,说明这个节点以及被修改成其他状态了,继续寻找下一个节点。

第三步把节点加到同步队列的尾部;

第四步根据同步队列的前置节点状态判断是否需要直接唤醒当前节点中的线程;

signal方法就是把Condition中维护的链表节点的头部节点状态设置为0并且加到AQS的同步队列中。signalAll方法就是把链表的所有节点走一下signal方法的流程。

Condition与Object

分析了Condition方法但是并没有体现它相比于Object的wait、notify、notifyAll的优势,实际上这个问题在之前的文章设计阻塞队列中有对比,这里大概讲一下,阻塞队列有put与take方法,当队列满了put方法会阻塞,当队列是空时take方法会阻塞。

用Object的wait、notifyAll方法来实现put、take方法的阻塞,在多线程情况下一个put方法获取到锁,那么所有的包括put方法和take方法都会阻塞,当put方法执行完成后会唤醒take线程,但同时put线程也会被唤醒来抢执行权,但是如果此时队列已满,实际上所有的put线程都不应该会唤醒。

如果采用ReentrantLock来new出两个Condition来实现,put方法如果发现队列满了则调用一个Condition的await方法来阻塞线程,另外一个来阻塞take方法,当put方法执行完后只用调用阻塞take方法中的Condition的signal,只用唤醒队列中最前面一个就够了。

总的来说可以利用一个ReentrantLock来实现对资源的访问控制,利用多个Condition来更加精准的控制线程的阻塞与唤醒,相比利用一个Object来控制更加准确,可以使程序运行的更加高效。

总结

最后再梳理下Condition的await、signal方法,再使用await方法之前必须调用Condition对应的ReentrantLock的lock方法,也就是必须要获取锁。线程要跳出await方法就必须把节点放到AQS中的同步队列中。而signal方法就是把节点放到同步队列中。

再整理下整个流程:lock方法直到获取锁、调用await方法(生成一个Node放到Condition链表的末尾)、然后释放锁、进入while循环挂起线程、另外的线程获取到锁调用signal修改Condition链表的头部节点状态并放到AQS同步队列中、当其他线程释放锁会去唤醒AQS同步队列头部线程、如果这个节点被唤醒就继续执行await的while判断会跳出循环、设置锁状态await方法执行完成、最后再调用unlock方法释放锁。

ReentrantLock用来维护同步队列保证只有一个线程访问资源、await方法把保存线程的节点加到Condition维护的链表中,signal方法把节点加到同步队列中,通过这样无论是await方法被唤醒还是其他地方ReentrantLock调用了lock都能保证资源只被最多一个线程访问。

ReentrantLock与Condition在其他并发类中经常遇到,值得弄得他们的原理与优势,便于理解其他并发类

Java程序员日常学习笔记,如理解有误欢迎各位交流讨论!

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20200809A0IZE500?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券