深入理解Java并发框架AQS系列(四):共享锁(Shared Lock) (2)

2、保证激活阻塞队列时的并发度;这个问题同样也是独占锁不存在的,也就是我们在3.1提出的问题;假定这样一种场景:“共享锁的并发度为10,阻塞队列中有100个待处理的节点,而此时又没有新的加锁请求,如何保证在激活阻塞队列时,保持10的并发度?”

共享锁如何解决这两个问题呢?我们接下来逐一阐述

3.2.1、调用点

与独占锁不同,共享锁调用“锁释放”有2个地方(注:AQS的一个阻塞队列是可以同时添加独占节点、共享节点的,为了简化模型,我们这里暂不讨论这种混合模型)

a、某线程同步块执行完毕,正常调用解锁逻辑;此点与独占锁一致

b、在每次更换头结点时,如果满足以下任一条件,同样会调用“锁释放”;更换头结点的操作,其实此时已经意味着当前线程已经加锁成功

b.1、有额外的资源可用;拿信号量举例,当发现信号量数量>0时,表示有额外资源可用

b.2、旧的头结点或当前头结点的ws < 0

那这两个点调用的时候,是否存在并发呢?有同学会说“a存在并发,b是串行的”;其实此处b也是存在并发的,例如线程1更换了head节点后,准备执行“锁释放”逻辑,正在此时,线程2正常锁释放后,唤醒了新的head节点(线程3),线程3又会执行更换head节点,并准备执行“锁释放”逻辑;此时线程1跟线程3都准备执行“锁释放”逻辑

共享锁解锁逻辑的调用点

既然“锁释放”存在这么多并发,那就一定要保证“锁释放”逻辑是幂等的,那它又是如何做到呢?

3.2.1、锁释放

直接贴一下它的源码吧,释放锁的代码寥寥几笔,却很难说它简单

private void doReleaseShared() { for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases unparkSuccessor(h); } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; } }

对应的流程图如下:

共享锁释放锁流程

我们简单描述一下锁释放做的事儿

1、首选获取头结点的快照,并将其赋予变量h,同时获取h.waitStatus,并标记位ws

2、判断ws的状态

ws == -1 表示下一个节点已经挂起,或即将挂起。如果只要发现是-1状态,就进行线程唤起的话,因为存在并发,可能导致目标线程被唤起多次,故此处需要通过CAS进行抢锁,保证只有一个线程去唤起

ws == 0 如果发现节点ws为0,此处会存在两种情况(情况1:节点刚新建完毕,还未进入阻塞队列;情况2:节点由-1修改为了0),不管哪种情况,都强制将其由-1改为-3,标记位强制传播,此处是否存在漏洞?

ws == -3 表示当前节点已经被标识为强制传播了,直接结束

3、如果此时 h == head,说明在上述逻辑发生时,头结点没有发生变化,那么结束当前操作,否则重复上述步骤。注:AQS中所有节点只有一次当头结点的机会,也就是某个节点当过一次头结点后,便会被抛弃,再无可能第二次成为头结点,这点至关重要

根据以上分析,我们发现,节点的状态流转是通过ws来控制的,即0、-1、-3,乍看上去,貌似不太严谨,那我们来做具体分析

3.2.2、ws状态流转

仅有2个功能点会对ws进行修改,一是将节点加入阻塞队列时,二就是3.2.1中描述的调用锁释放逻辑时;

我们将加入阻塞队列时ws的状态流转再回忆下:

状态为0(初始状态),加入阻塞队列前,需要将前节点修改为-1,然后进入线程挂起

状态为-3(强制传播状态,被解锁线程标记),加入阻塞队列前,同样需要将前节点修改为-1,然后进入线程挂起

综述,我们出一张ws的整体状态流转图

共享锁加入阻塞队列时ws流转

由上图可得知,只要解锁逻辑成功通过CAS将head节点由-1修改为0的话,那么就要负责唤醒阻塞队列中的第一个节点了

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zzgsdj.html