AQS源码解读(六)——从PROPAGATE和setHeadAndPropagate()分析共享锁的传播性

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注Java)
img

正文

五、setHeadAndPropagate共享锁的传播性


node获取锁成功出队,设置新head,并将共享性传播给后继节点,即唤醒后继共享节点。为什么当一个节点的线程获取共享锁后,要唤醒后继共享节点?共享锁是可以多个线程共有的,当一个节点的线程获取共享锁后,必然要通知后继共享节点的线程,也可以获取锁了,这样就不会让其他等待的线程等很久,而传播性的目的也是尽快通知其他等待的线程尽快获取锁。

private void setHeadAndPropagate(Node node, int propagate) {

Node h = head; // Record old head for check below

//设置node为新head

setHead(node);

/*

  • The conservatism in both of these checks may cause

  • unnecessary wake-ups, but only when there are multiple

  • racing acquires/releases, so most need signals now or soon

  • anyway.

*/

// propagate > 0,短路后面的判断

if (propagate > 0 || h == null || h.waitStatus < 0 ||

(h = head) == null || h.waitStatus < 0) {

//唤醒后继共享节点

Node s = node.next;

if (s == null || s.isShared())

doReleaseShared();

}

}

setHeadAndPropagate中调用doReleaseShared前需要一连串的条件判断,大概可以分为三部分:

1. propagate > 0

ReentrantReadWriteLock中走到setHeadAndPropagate,只可能是propagate > 0,所以后面判断旧、新head的逻辑就被短路了。

而在Semaphore中走到setHeadAndPropagatepropagate是可以等于0的,表示没有剩余资源了,故propagate > 0不满足,往后判断。

2. h == null || h.waitStatus < 0

首先判断旧head是否为null,一般情况下是不可能是等于null,除非旧head刚好被gc了。h == null不满足,继续判断h.waitStatus < 0h.waitStatus可能等于0,可能等于-3。

  • h.waitStatus=0的情况,某个线程释放了锁(release or releaseShared)或者前一个节点获取共享锁传播setHeadAndPropagate,唤醒后继节点的时候将h.waitStatus=-1设置为0。

  • h.waitStatus=-3doReleaseShared唤醒head后继节点后h.waitStatus从-1到0,还没来得及更新head,即被唤醒的共享节点还没有setHeadAndPropagate,又有其他线程doReleaseShared唤醒head后继节点h.waitStatus从0到-3。

//java.util.concurrent.locks.AbstractQueuedSynchronizer#unparkSuccessor

private void unparkSuccessor(Node node) {

int ws = node.waitStatus;

if (ws < 0)

//cas设置h.waitStatus -1 --> 0

compareAndSetWaitStatus(node, ws, 0);

//唤醒后继节点的线程,若为空or取消了,从tail往后遍历找到一个正常的节点

Node s = node.next;

if (s == null || s.waitStatus > 0) {

s = null;

for (Node t = tail; t != null && t != node; t = t.prev)

if (t.waitStatus <= 0)

s = t;

}

if (s != null)

//uppark线程

LockSupport.unpark(s.thread);

}

当释放共享锁or共享锁传播后会调用doReleaseShared唤醒同步队列中head的后继节点。

首先明确几个判断:

  • h.waitStatus = Node.SIGNALcompareAndSetWaitStatus(h, Node.SIGNAL, 0))unparkSuccessor

  • h.waitStatus = 0compareAndSetWaitStatus(h, 0, Node.PROPAGATE)设置head为传播模式。

  • h == head,head没有变,break中断循环;也可能被唤醒的节点立刻获取了锁出队列,导致head变了,所以继续循环唤醒head后继节点。

//java.util.concurrent.locks.AbstractQueuedSynchronizer#doReleaseShared

private void doReleaseShared() {

for (;😉 {

Node h = head;

if (h != null && h != tail) {

int ws = h.waitStatus;

if (ws == Node.SIGNAL) {

//SIGNAL --> 0

if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))

continue; // loop to recheck cases

//唤醒后继节点的线程

unparkSuccessor(h);

}

else if (ws == 0 &&

//0 --> PROPAGATE

!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))

continue; // loop on failed CAS

}

/**

  • we must loop in case a new node is added

  • while we are doing this

*/

if (h == head) // loop if head changed

//head没有变则break

break;

}

}

3. (h = head) == null || h.waitStatus < 0

首先判断新head是否为空,一般情况下新head不为空,(h = head) == null不满足,判断h.waitStatus < 0h.waitStatus可能等于0,可能小于0(-3 or -1)。

  • h.waitStatus可能等于0的情况,后继节点刚好入队列,还没有走到shouldParkAfterFailedAcquire()中的修改前继节点waitStatus的代码。

  • h.waitStatus=-3,上一个共享节点被唤醒后,成为新head,后继节点刚入队列,又有其他线程释放锁调用doReleaseSharedh.waitStatus从0改为-3。

  • h.waitStatus=-1,已经调用了shouldParkAfterFailedAcquire()h.waitStatus从0 or -3 改为-1,可能阻塞,可能未阻塞。

//java.util.concurrent.locks.AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {

int ws = pred.waitStatus;

if (ws == Node.SIGNAL)

/*

  • This node has already set status asking a release

  • to signal it, so it can safely park.

  • node拿锁失败,前继节点的状态是SIGNAL,node节点可以放心的阻塞,

  • 因为下次会被唤醒

*/

return true;

if (ws > 0) {

/*

  • Predecessor was cancelled. Skip over predecessors and

  • indicate retry.

  • pred节点被取消了,跳过pred

do {

node.prev = pred = pred.prev;

} while (pred.waitStatus > 0);

pred.next = node;

} else {

/* 0 -3

  • waitStatus must be 0 or PROPAGATE. Indicate that we

  • need a signal, but don’t park yet. Caller will need to

  • retry to make sure it cannot acquire before parking.

*/

compareAndSetWaitStatus(pred, ws, Node.SIGNAL);

}

return false;

}

六、举例求证共享锁传播性


接下来通过控制变量法,求证共享锁的传播性。

对于ReentrantReadWriteLock,既然走到了setHeadAndPropagatetryAcquireShared返回值就一定大于0,即propagate > 0,所以对于ReentrantReadWriteLockPROPAGATE只是一个中间状态值,即使没有PROPAGATE也不会影响共享锁的传播性。

Semaphore具有资源的概念,走到setHeadAndPropagatetryAcquireShared返回值代表资源剩余量,返回值可能等于0,所以需要配合状态PROPAGATE完成共享锁的传播性。

接下来只讨论Semaphore的传播情况。

(1)假设Semaphore有10个资源都被占用。A线程获取资源失败入队列阻塞。此时C释放资源,唤醒A(h.ws=-1 ---> h.ws=0)。A获取资源成功,但是没有资源了,tryAcquireShared返回0(p=0)。此时B线程获取资源失败进入队列,但是还没有阻塞(A.ws=0)。

p>0不满足,h==null || h.ws < 0 不满足,h=head==null || h.ws < 0不满足。都不满足,A线程执行setHeadAndPropagate不唤醒B线程,因为没有资源了,没必要唤醒。

(2)在(1)的基础上,当判断完3个条件后,D线程释放资源,B还没有阻塞,B也无需被D唤醒,B在阻塞前还有一次重试的机会;在D释放资源前,B已经阻塞了,就由D唤醒B。

(3)在(1)的基础上,当在判断3个条件前,B已经阻塞了,h=head==null || h.ws < 0判断新head满足条件,A唤醒B,B可能因为没有资源而获取资源失败继续阻塞,造成了不必要的唤醒,也可能因为此时刚好有线程释放了资源,B获取资源出队列。

(4)在(1)的基础上,C唤醒A后,A获取锁成功,tryAcquireShared返回0,A还未出队列,此时B又刚好获取锁失败进入队列。此时D释放资源h.ws=0 ---> h.ws=-3。p>0不满足,h==null || h.ws < 0满足,执行doReleaseShared唤醒B。B还没有阻塞没必要唤醒,若B没有抢过其他线程而阻塞,此时唤醒B,B可能因为没有资源而获取资源失败继续阻塞,造成了不必要的唤醒,也可能因为此时刚好有线程释放了资源,B获取资源出队列。

在这里插入图片描述

(5)在(1)的基础上,C唤醒A后,A获取锁成功,tryAcquireShared返回0,A刚出队列,还没执行到3个判断,B获取资源失败进入队列还未阻塞。此时D释放资源,新head h.ws=0 ---> h.ws=-3p>0不满足,h==null || h.ws < 0 不满足,h=head==null || h.ws < 0满足,执行doReleaseShared唤醒B。B还没有阻塞没必要唤醒,若B没有抢过其他线程而阻塞,此时唤醒B,B可能因为没有资源而获取资源失败继续阻塞,造成了不必要的唤醒,也可能因为此时刚好有线程释放了资源,B获取资源出队列。

在这里插入图片描述

假设没有状态PROPAGATE

  • 线程多次调用doReleaseShared,保持head的waitStatus为0,(4)和(5)在B没有阻塞的前提是不会继续doReleaseShared,若B线程在3个判断后阻塞,此时B就需要等下一个线程释放资源唤醒,这样有可能会导致B等待时间过长。

  • 若多次doReleaseShared,head的waitStatus改为-1,也是不合理的,这样可能会导致B还没有阻塞直接就判断应该阻塞,丧失了重试一次的机会。

七、总结


  • ReentrantReadWriteLockPROPAGATE只是一个中间状态,共享锁的传播性由setHeadAndPropagate完成。

  • 对于有资源概念的SemaphorePROPAGATEsetHeadAndPropagate组合完成共享锁的传播性。

  • 共享锁的传播性目的是尽快唤醒同步队列中等待的线程,使其尽快获取资源(锁),但是也有一定的副作用,可能会造成不必要的唤醒。

  • PROPAGATE只设置给head的waitStatus,让head节点具有传播性。

  • PROPAGATE作为中间状态的流转(h.ws=0 ---> h.ws=-3 ---> h.ws=-1)和临界判断(h.ws < 0)。

  • 出现h.ws=0 ---> h.ws=-3的情况:

  1. 有一线程获取共享锁后唤醒后继节点(h.ws=-1--->h.ws=0),这时有另一个线程释放了共享锁(h.ws=0--->h.ws=-3)。(ReentrantReadWriteLockSemaphore都可能有这种情况)

  2. 有一线程释放了共享锁(h.ws=-1--->h.ws=0)又有一线程释放了共享锁(h.ws=0--->h.ws=-3)。(Semaphore可能有这种情况,ReentrantReadWriteLock不可能,因为ReentrantReadWriteLock不是每次释放共享锁都会唤醒head后继节点,必须完全释放锁)

总结

对于面试还是要好好准备的,尤其是有些问题还是很容易挖坑的,例如你为什么离开现在的公司(你当然不应该抱怨现在的公司有哪些不好的地方,更多的应该表明自己想要寻找更好的发展机会,自己的一些现实因素,比如对于我而言是现在应聘的公司离自己的家更近,又或者是自己工作到达了迷茫期,想跳出迷茫期等等)

image

Java面试精选题、架构实战文档

整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~

你的支持,我的动力;祝各位前程似锦,offer不断!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

对于我而言是现在应聘的公司离自己的家更近,又或者是自己工作到达了迷茫期,想跳出迷茫期等等)

[外链图片转存中…(img-2QN45dmz-1713595987218)]

Java面试精选题、架构实战文档

整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~

你的支持,我的动力;祝各位前程似锦,offer不断!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-nJkw0hLm-1713595987219)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 11
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值