Java基础之AQS设计思路介绍
为了更好的学习Java中的AQS,先学习下AQS的设计思路。
AQS以及synchronized都是管程模型的实现,了解AQS整体的设计,可以先了解下管程的实现。
AQS的需求
- 功能需求:提供两种基本操作acquire和release
- 性能需求:允许定制化公平策略、尽可能缩短释放锁和获取锁之间的时间、平衡CPU和内存的资源消耗
核心思路
- 获取锁的线程需要排队,可满足公平性
- 选择性的阻塞正在等待获取锁的线程,这样在竞争激烈的时候,可以让资源消耗控制在一个可预测的范围内;同时允许一定程度自旋,在竞争少的情况的尽可能缩短释放锁和获取锁之间的时间消耗
- 释放锁的时候,根据情况唤醒一个或者多个线程,平衡竞争效率和公平性的需求。
由思路引出要解决的问题:
- 如何原子性管理同步器状态
- 如果维护一个队列
- 如何阻塞和唤醒线程
AQS中同步器状态的表示
使用一个volatile类型的int变量,即可以表示锁类型同步器的状态,也可以表示计数类型的同步器状态(如Semaphore)。
AQS中的CLH队列锁
AQS的核心就是阻塞线程队列的管理,AQS使用了CLH的变种,对原始的CLH做了修改和调整。
AQS的CLH队列锁增加了前驱和后继指针
CLH原始设计中没有使用掐去和后继指针关联,AQS添加了前驱和后继指针,通过前驱指针,AQS可以处理获取锁过程中的超时和取消,如果一个结点的前驱结点对应线程取消了对锁的等待,当前结点可以利用前驱指针读取更前面的结点状态,用于判断自己是否可以获取锁。
通过后继指针,可以帮助当前结点快捷的找到后继结点,进行唤醒。后继指针是不可靠的,如果视图从队列头部通过后继指针遍历整个队列,可能某个结点的后继指针为空,但是实际该节点的后继已经追加了一个甚至多个结点,所以当通过后继指针找不到后继结点时,必须需要从尾部依靠前驱指针反向遍历一下,才能判断结点的后继是否真的没有结点。
AQS修改了CLH锁获取的判定条件
CLH锁获取原始设计如下:
每个结点有一个状态变量,每个线程自旋判断前驱结点的状态变量,用来判断自己能否获取锁,当一个结点释放锁的时候,会修改自己的结点状态,用来通知后继结点可以结束自旋。
AQS做了如下调整:
- CLH原始设计中每个节点自己的状态变量被抽取出来,变为整个队列可见的公共变量。
- AQS添加一个head指针,当持有锁的线程释放锁的时候,会将head指针指向这个线程对应的节点,通知后续线程可以尝试获取锁
- 一个线程通过判断head的位置,决定自己是否可以获得锁。队列中头结点线程可以通过cas方式原子修改状态变量,修改成功就获得了锁
AQS的CLH队列为结点增加了waitStatus变量
waitStatus变量有如下表示信息:
- 可表示线程是否取消了锁的等待
- 可表示线程是否需要唤醒下一个等待线程
- 可表示条件变量的等待
- 可表示共享状态
AQS如何阻塞和唤醒线程
JSR166之前只有Thread.suspend和Thread.resume,但是如果一个线程调用了resume后调用suspend,则这个resume不会产生任何作用。JSR166后增加了LockSupport来解决这个问题,park阻塞当前线程,unpark唤醒线程,如果在park之前调用unpark,park不会阻塞线程,也就是说unpark可以先于park调用。
unpark操作不计数,在park之前多次调用unpark,park调用时不会阻塞线程,如果再次调用park,则会阻塞线程。
park和unpark还支持超时和中断。
acquire操作
if (尝试获取锁不成功) {
node = 创建新结点,并入队列
pred = 结点的前驱结点
while (pred 不是头结点 || 尝试获取锁失败) {
if (前驱结点的waitStatus是SIGNAL) {
将当前线程挂起
} else {
cas设置前驱结点的waitStatus为SIGNAL
}
head = node
}
}
release操作
if (尝试释放锁 && 头结点的waitStatus是SIGNAL) {
cas设置头结点waitStatus不是SIGNAL
唤醒后继结点
}