来源:架构染色
一、满是疑惑的 AQS 介绍
AQS,全称 Abstract Queued Synchronizer,是一个抽象的队列同步器,通过维护一个共享资源状态( Volatile Int State )和一个先进先出( FIFO )的线程等待队列来实现一个多线程访问共享资源的同步框架。
看完这段描述中,大家或多或少会有以下几个疑问
本篇就围绕这些疑问,咱们一起从网络中收集收集资料,从 AQS 的概念和功能设计层面来一波挖呀挖。笔者之前学习 大师论文时也有记录一篇《自省|AQS 中的同步队列为何这样设计》,感兴趣的老师也可以结合本篇看一看。
二、挖一挖同步的概念?
操作系统引入并发程序设计技术,但并发执行的程序由于共享系统资源,在协同完成任务时还需要交互,从而产生进程/线程之间的相互依赖和相互制约的关系,即协作关系和竞争(互斥)关系。
小纸条:
这里提到了竞争/互斥 和 协作 这几个关键字,其中把竞争关系也叫做了互斥关系;可是没有同步哇?
2.1 共享系统资源可能有问题
若并发程序共享一些资源,比如,内存,文件,数据库等。当多个进程/线程同时读写同一份共享资源的时候,可能会引起冲突,也可能出现一致性问题。
2.2 什么是互斥?
2.3 什么是同步?
2.4 互斥是特殊的同步
2.5 强大的同步机制
三、挖一挖 AQS 的设计?
1) 共享系统资源可能有问题,我们需要注意
2)使用同步来避免这类问题?
3)同步是什么呢?
4)为什么用队列?
5)为什么叫同步器?
6)为什么没有互斥器?
7)为什么是抽象的
Semaphore、CountDownLatch、CyclicBarrier等。8)怎么访问共享资源?
互斥模式访问共享资源
共享模式访问共享资源
Condition 协作模式访问共享资源
注意:
这里用到放行这个词,大家看到的较多的资料都不用这个词,为什么笔者这里这么说呢,因为大多资料讲的都是从锁的视角来解释独占模式下只允许一个线程抢锁。但共享模式下按照锁的视角来说则允许多个线程同时持锁,如果持锁了就不用排队等待,这在现实场景中很类似被放行,可继续执行 lock 后的逻辑。
四、挖一挖关键源码?
很多博主有针对 AQS 的源码实现,逐行解释其实现细节,但往往让读者老师看着很懵,所以笔者本篇的初衷是从功能层面,描述其主体逻辑和不同上下文下的扩展性,有了宏观视角,再结合其他博主的文章(后续笔者也可能整理一篇)可能会效果更好,希望能帮助大家。
4.1 管理队列
AQS 是由其 head 和 tail 属性维护一个双向队列,线程被包装成节点,然后在队列中抢占(注意这里用抢占一词)合适的位置
另外一个关键点队列管理的关键就是队首和队尾的值,即 AQS 对象中 head 和 tail 的赋值
这里的赋值操作出于性能考虑,赋值采用了性价比最高的自旋+CAS。
但 CAS 的机制也导致只能保障 head 或 tail 中其中一个值的赋值是安全的,而无法同时保障 head 和 tail 两个变量赋值的原子性
所以双向队列方向肯定只有一个方向是绝对可靠的,实际上就是 tail->head 的方向是安全可靠的,也就是说当新节点加入队列后,可能有那么一瞬间无法从 head-> tail 方向遍历找到节点,那么此时可采用从 tail-> head 方向遍历
为什么要维护 head -> tail 方向的链路呢?想象一下当你休眠后,你的前继节点完事后,直接唤醒作为其后继节点的你去访问共享资源;这种直接的操作在大多数情况下就要比从 tail -> head 方向遍历效率要高。
当然队列的管理还有线程节点在条件队列和同步队列之间的迁移,原理相对比较简单,此处暂不多说
4.2、acquire 方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(
addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE)
, arg)
)
selfInterrupt();
}
不难发现,这里有三个方法,他们非常核心,但在进入代码层面介绍之前先回顾一下上文所介绍的几个关键内容:
这 3 个逻辑便是围绕以上两条主体逻辑而做的实现,我们分别来看一下其核心逻辑
1)无需排队时
tryAcquire()
2)可能需要排队时
addWaiter()
acquireQueued()
这个环节有休眠和被唤醒后申请同步器放行两个核心逻辑
如果醒后申请放行失败,则继续休眠,所以源码中是个循环,直到成功才退出循环。
若申请放行不成功,交代前驱节点唤醒自己(前驱节点打个标记),之后休眠释放掉时间片。当前驱节点释放后,通过 unpark 唤醒后继节点的线程;线程被唤醒后,继续申请放行;这一步是一个循环
另外从逻辑前后顺序来说,是先睡眠再申请放行还是先申请放行再睡眠呢,其实从循环执行的视角看都可以,但从性能的视角看稍有偏差,在第一次休眠前还有申请放行的机会。
注意:
三个方法内,都有申请放行,因为当前时间点不满足,而很可能下一个逻辑执行时就满足了放行的条件,所以有多次尝试;这几次尝试通常也被称为自旋。
