AQS中传播状态(PROPAGATE)的个人理解

背景介绍

最近又看到一些AQS的介绍文章,然后对共享模式有些困惑,在有些理解之后,特此记录,仅代表个人观点。

注:阅读这篇文章需要对AQS代码执行有些了解,建议参考后面的理解先阅读一下

AQS的基本原理

一个基于CLH队列扩展出来的实现,实现了自旋到阻塞,以及提出了节点的共享模式。核心是用一个状态变量数值的修改标识竞争资源的可用状态,后面把这个状态叫做“锁”。

头结点标识所有目前获取到“锁”的线程,是“一对多”的关系,所以才有设置头结点需要把thread置空的操作,因为一个thread对象代表不了所有正在运行的线程。

所有通过tryAcquire和tryAcquireShared没有获取到“锁”的线程都会被挂起,才形成了等待队列的节点,所有没在这里的都是获取到“锁”的线程。

图就不画了,可以参考下面的CLH连接博客做个理解。

传播状态怎么理解

首先我们理解一下节点的共享模式(share mode),类比Mysql的读写锁,读其实对于多线程是安全的,不需要做什么特别的同步设置。

那么假如队列中的某个等待读锁的线程A被触发获取到了,很合理的一个想法是把后面处于共享模式并且已经挂起(park)的节点一并唤醒,这一个过程就是所谓的“传播”。

备注:这里有个比较重要的思想就是,所有入队之后的节点(代表线程)获取锁的执行流程都是类似的(具体方法上只是增加了一些像是时限等的限制,具体业务操作流程还是一样的)。

针对共享模式,其中就包括入队--->尝试获取共享资源--->设置本节点为头结点--->判断后置节点如果为共享模式,则尝试唤醒(传播)

 如果节点已经阻塞被唤醒,则不包括入队操作。

假如现在有个共享节点(线程B)被唤醒,就要走绿色的几步,假如在尝试获取共享资源这一步比较耗时,前面进行唤醒工作的线程A还需要继续唤醒吗?

好像不太合理,毕竟谁知道线程B需要获取几个“许可”,决定权在B手里,那线程A为了执行的效率,是不是应该不去做后续的唤醒操作,由线程B去操作就好了。

线程A的“传播”任务需要交给线程B,怎么交?——就是通过  Node.PROPAGATE = -3 这个状态去提醒,

体现在代码层面就是,waitStatus小于0

 

 其实,从上面的描述中也看出来,在实现上唤醒传播应该是一个节点一个节点操作的,从头结点开始,体现在源码层面

 

 假如头结点没有发生变化(h == head),说明本次传播已经确实交付,下一个节点B还没有到设置头结点这步,如果到了,会判断到 ws < 0(见setHeadAndPropagate)进行传播。

大概理解到这里吧,可能会有些不对的地方,总体模型应该是我这样理解的。应该算是个参考吧。

吐槽

其实现在很多这个、那个的原理解读,个人感觉有些贩卖焦虑的赶脚了,很多都是直接从源码入手告诉你怎么实现。

其实这种方式是有问题的,源码中的方法只是作者构思的实现的“局部”而已,自己体会一下,让你实现一个功能,你会库次一下就直接从第一行代码开始吗?

怎么也会构思一下怎么实现吧(就是算法),然而这就是很多原理文章欠缺的,给人一种盲人摸象的感觉,摸到哪了,哪里就是怎么样,好似大象一直在那,我们就是去摸摸就能了解到了一样。然后阅读完一篇文章之后,就会留下一种潜意识——就是东西就该是这样的,我们只要去了解就行了,不会去更多的想为什么会这样。

不过这也达到了某些文章想要的效果,那就是——我就是牛,我每一行都懂,我每一行代码都可以告诉你,但是我相信你就是不懂,想学啊?关注我,问我啊。哈哈,俗称装逼。

又或许是因为文章的“一维”特点导致了不能很好的表示某些东西吧,具体原因仁者见仁智者见智了。

授人以鱼真不如授人以渔,以后自己看文章也需要多思考一下为什么要这样实现,而不是这样有什么功效。

归根结底,脑海中需要有一个问题场景,就是这套代码研究的对象还有对象之间的交互形式。就像我上篇文章讲的那样,需要一个问题模型。

不过还是非常感谢各位博主的无私分享滴~~~

杂记

讲一下我自己目前是如何理解线程之间的同步问题吧,我的理解是线程之间如果操作同一块内存状态了,就涉及到同步了,然后分析的时候就需要把涉及到这块内存的操作枚举出来,做一个排序,在AQS中大概就是这样,

 这只是大概的方向,更难的是通过像是锁和CAS的基本技术实现那块内存状态保持确定(并发的有序性、可见性以及原子性问题)的状态变化(状态机的概念),比如上面讲的唤醒任务的递交,具体实现过程中就需要分析各种可能的操作交互形式(形象的来理解就是上图左右两个线程上下挪动将操作顺序进行交错,当然这个只是简单理解,因为中间还涉及到内存可见性以及指令重排序问题),这个才是最复杂的。

然后AQS巧妙的地方就是将线程之间的同步维护在一个队列中,队列的操作从来都是从前往后(从头结点开始),由此在各个线程入队之后限制了线程之间随意交互的大多数不确定性(因为后面大部分入队的节点的前置节点都不是头结点,没法进行任何操作),只需要解决相对较少的不确定性就行了。

这里说到状态机,提一嘴,从源码的注释(见上面setHeadAndPropagate)以及前面的分析来看,SIGNAL 看着像是 PROPAGATE 的子状态,节点处于SIGNAL 状态,一定满足PROPAGATE 的要求。

小结

乱七八糟的说了一通,大家挑着看就行。

 

参考文章

CLH队列基本介绍:https://juejin.cn/post/6864210697292054541

 

深入浅出AQS之共享锁模式 - 凌风郎少 - 博客园 (cnblogs.com)

 

面试必考AQS-共享锁的申请与释放,传播状态 - 知乎 (zhihu.com)这篇讲的没大看懂,不过核心:找出一切可能的情况,去通知后继节点干活,去最大可能的尝试获取锁。感觉还是挺有道理的。

tryAcquireShared
posted @ 2022-12-12 04:44  MarshWinter  阅读(318)  评论(0编辑  收藏  举报