所属文集:一起掌握并发
1.前情概要
本篇是阅读论文《The java.util.concurrent Synchronizer Framework》 JUC同步器框架(AQS框架)原文翻译 ,并结合AQS的源码阅读之后,自己的一些思考总结。
AQS的ReentrantLock的源码阅读后,梳理出来的脑图:https://www.processon.com/view/link/5e200b65e4b0dd06803a71e9
2.自己的理解与思考
互斥:是让线程交替执行一段代码,而不能同时执行;换一个说法就是线程竞争到锁就执行同步代码,争取不到锁(同步状态)就不能执行同步代码,只能等待。
这里等待有3中方式:
- 争取不到锁,线程休眠等待,
- 争取不到锁,线程自旋等待,
- 争取不到锁,线程先自旋等待一会儿,不行再休眠等待(AQS的做法)。
锁:AQS中,是否加锁状态,使用int类型状态变量来记录,通过CAS操作来修改其值;0表示无锁,1表示加锁,>1表示重入,重入时执行++操作,记录重入次数;释放锁时执行--操作,直到减到0才表示锁被完全释放,可被其他线程抢占。
休眠:休眠和唤醒使用unsafe中的park(休眠线程)和unpark(唤醒线程)方法。
自旋:在java语言中的for循环,循环次数是有限的。
等待:等待使用双向队列的结构来实现排队等待的效果,不能插队的模式是公平模式,可插队模式是非公平模式。
2.1 单向队列的需求和设计:
引用原文:
在原始版本的CLH锁中,节点间甚至都没有互相链接。自旋锁中,pred变量可以是一个局部变量。然而,Scott和Scherer证明了通过在节点中显式地维护前驱节点,CLH锁就可以处理“超时”和各种形式的“取消”:如果一个节点的前驱节点取消了,这个节点就可以滑动去使用前面一个节点的状态字段。
立即原文:可以比较容易的处理超时和取消类型的请求,在队列的组织结构下,前一个请求因为取消或者超时而变得报废无效了,后边的请求往前挤压,可以把无效的移除队列,以便让处于存活有效状态的向前移动。
为什么选择tail向head方向(pre链路)?
从tail到head,即pre方向更自然,队末的更有需求让队伍往前走,因为自己的事还没办;队首的自己的事已经办完了,后边排队的跟自己关系不大。
比如堵车的时候,总是后车的打喇叭催促前车快走。如果前车故障停下不走(类比线程超时、取消),后车因为要往前走,就让前车靠边,自己绕过继续前行(类比tail向前滑动,将取消、中断的移出队列)
2.2 自旋修改为自旋+阻塞
1.原文说
第二个对CLH队列主要的修改是将每个节点都有的状态字段用于控制阻塞而非自旋”,... “取消”状态必须存在于状态字段中"。
把面向自旋的设计修改为了面向阻塞:
先看自旋的设计,非常容易理解:
每一个节点的“释放”状态都保存在其前驱节点中。因此,自旋锁的“自旋”操作就如下:
while (pred.status != RELEASED); // spin
自旋后的出队操作只需将head字段指向刚刚得到锁的节点:
head = node;
但是全部是自旋也浪费CPU,修改成阻塞试试:
while (pred.status != RELEASED){
阻塞休眠
}
所有线程一上来,看状态不满足,就阻塞也不合适;比如第一个排队的,跟其他后续排队的情况不同,对于第一个排队的,其前边的可能已经或者很快就结束,这种情况下最好先自旋几此尝试去竞争锁,如果失败了再阻塞,而其他后续的排队的节点就可以上来就排队阻塞。
原文中”检查当前节点的前驱是否为head来确定权限"也是这个意思,即不用判断前驱节点的release状态,只需要判断是不是head,是head就尝试竞争锁,不是head就阻塞(park)排队,等着被唤醒。
2.3 双向队列的需求和设计:
线程拿不到锁会排队阻塞,后续就需要被唤醒;如何唤醒后继的节点,前边提到 队列被设计为tail 到 head方向的链路;从tail顺着pre往head方向找总能找到,但是优化一下,能直接找到自己的后继节点就会更方便。所以增加next方向,队列就变成了双向。但是双向的链路控制不是原子的,保证pre方向及时有效,next方向略有延迟,所以,在next方向找不到的情况下,要尝试从tail沿着pre方向往前找一下。
2.4 后继节点如何阻塞,如何唤醒
当前节点release后,后继节点可能正在自旋竞争锁呢,这种情况下没有必要去唤醒它(虽然unpark唤醒操作也不会出错,但也是有成本的);出于成基考虑再优化一下,在休眠之前,最好给前驱节点个“唤醒(signal me)”自己的信号。
所以线程调用park前,给前驱节点设置一个“唤醒(signal me)”标志,并再尝试一次去拿锁,如果竞争到了锁,就避免了一次不必要的阻塞;如果竞争不到锁,才去真的休眠。
2.5 队列延迟初始化
只有一个线程的时候,或者线程是交替执行,且线程之间无交叉,这种情况下,线程都不需要排队,只需要判断同步状态,修改同步状态;因此队列是在首次需要的时候才进行初始化(构建一个虚拟节点,head和tail都指向它)。
2.6 排队策略:公平与非公平
非公平:插队模式,往队首插队,插不进去,才追加到队尾。
公平:不可插队模式,到队尾排队。