细节爆炸!并发编程的半壁江山——AQS详解( 三 )


细节爆炸!并发编程的半壁江山——AQS详解文章插图
结点状态waitStatus,需要保证可见性,用volatile修饰 这里我们说下Node 。 Node结点是对每一个等待获取资源的线程的封装 , 其包含了需要同步的线程本身及其等待状态 , 如是否被阻塞、是否等待唤醒、是否已经被取消等 。 变量waitStatus则表示当前Node结点的等待状态 , 共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0 。

  • CANCELLED(1):表示当前结点已取消调度 。 当timeout或被中断(响应中断的情况下) , 会触发变更为此状态 , 进入该状态后的结点将不会再变化 。
  • SIGNAL(-1):表示后继结点在等待当前结点唤醒 。 后继结点入队时 , 会将前继结点的状态更新为SIGNAL 。
  • CONDITION(-2):表示结点等待在Condition上 , 当其他线程调用了Condition的signal()方法后 , CONDITION状态的结点将从等待队列转移到同步队列中 , 等待获取同步锁 。
  • PROPAGATE(-3):共享模式下 , 前继结点不仅会唤醒其后继结点 , 同时也可能会唤醒后继的后继结点 。
  • 0:新结点入队时的默认状态 。
注意 , 负值表示结点处于有效等待状态 , 而正值表示结点已被取消 。 所以源码中很多地方用>0、<0来判断结点的状态是否正常 。
同步队列中节点的增加和移除
细节爆炸!并发编程的半壁江山——AQS详解文章插图
通过图可以看出来,在增加 尾节点的时候需要通过CAS设置,因为可能是多个线程同时设置,但是移除首节点的时候是不需要的,因为这个操作是由同步器操作的,并且首节点只有一个
独占式同步状态的获取与释放
细节爆炸!并发编程的半壁江山——AQS详解文章插图
AQS的从线程的获取同步状态到,对同步队列的维护,到释放,的流程图就是这样的,有兴趣看源码的自己去跟一下,就是主要实现的模板方法,
注意:其实在这个给大家提个醒,看源码的时候,找核心的看,找主要的看,不要一行一行地扣着看,没有意义,还有就是调用过程复杂,体会核心流程就可以
之前写了<>这一章,然后在这里写一下Condition接口在AQS里面的实现吧,因为不管自己写锁也好,默认锁的实现也好,用的Condition都是AQS默认写好的
Condition实现分析:
细节爆炸!并发编程的半壁江山——AQS详解文章插图
一个锁是可以有多个Condition的,每个Condition都包含一个自己的等待队列,不同于Object属于同一个对象等待,他存在一个单链表结构的等待队列,清晰的知道要唤醒自己的等待队列中的节点,所以采用signal方法而不是signalall
当然采用的类还是Node类当然单链表其实就是没有上一个节点的引用而已
等待队列和同步队列采用的是相同的类,只不过是实现的数据机构确是不一样的而已
最终一个锁的实例化会成为上图中第二个图的这种形式,Demo也就是之前写的<>中的用的锁最终形成的结构及时就是维持了一个同步队列和两个等待队列,锁用于控制并发,而两个队列用于控制地点变化和公里数变化的不同的等待通知模式
节点在队列中的移动
细节爆炸!并发编程的半壁江山——AQS详解文章插图
就是在当前线程await的时候从同步队列移除后加入到等待队列尾部,而唤醒就是从等待队列移除后加入到同步队列尾部,两个队列相互转换的过程,之所以采用同一个类,就是为了方便的在不同队列中相互转化
当然这也是为什么不推荐使用SignalAll方法的原因,因为如果一个等待队列中有很多的线程在等待,全部唤醒后,最多且只能有一个线程获取到同步状态,其他线程全部要被加入到同步队列的末尾,而且也可能当前的同步状态被别人持有,一个线程也获取不到,全部都要被加入同步队列中,所以不推荐使用SignalAll,推荐是用Signal
其实也可以想象,比如wait和notify/notifyAll 在写<<线程之间的协作(等待通知模式)>>这篇文章的时候的最后一个问题也可以大概想象一下,应该也是维持了一个同步队列,但是等待队列应该是只有一个,所以,被唤醒的是第一个等待的节点,但是它没有办法保证要被唤醒的节点一定是在头一个,只能唤醒全部的节点,来保证需要唤醒的线程一定被唤醒,大概也是这样的一个节点的移动,根据网络文章的描述,应该八九不离十
根据猜测,结合上方的Condition接口分析,所以说,在wait,notify/notifyAll中推荐使用notifyAll,防止第一个节点不是需要唤醒的节点,造成唤醒错误,但是Condition是知道的,被唤醒的一定是需要唤醒的,不会唤醒错误,所以说,推荐使用signal