1、入同步队列
当 AQS
在尝试获取锁失败时,会将当前线程构造成 Node
节点,插入同步队列中。
在说入队列操作之前,需要对 Node
的数据结构进行一下说明
1.1、同步队列 数据结构
同步队列 的数据结构为 双向链表。
Node
中 waitStatus
值的定义非常关键。
// 线程已取消
static final int CANCELLED = 1;
// 线程需要被 unpark
static final int SIGNAL = -1;
// 线程在等待
static final int CONDITION = -2;
// 下一个 acquireShared 需要被传播
static final int PROPAGATE = -3;
volatile int waitStatus;
1.2、入队列流程
tryAcquire
失败后,线程会被构造为Node
节点加入到同步队列。- 入队列(addWaiter)
2.1. 如果 tail == null
构造一个不指向任何线程的 Node 节点,并设置为 head
,tail
2.2. 如果 tail != null
1. 尝试快速插入:尝试将当前节点设置为尾节点,如果成功则直接返回。这里的指针操作细节为:先将当前节点 `prev` 指针指向 `tail`, 再用 `CAS` 将当前节点设置为尾节点, 最后再将 `tail` 的 `next` 指针指向当前节点
2. 如果尝试快速插入失败,则会使用自旋的方式,执行上诉步骤直到成功
- 节点自旋获取锁(acquireQueued)
3.1. 获取当前节点的上一个节点 prev
3.2. 如果 prev
为头节点,且 tryAcquire
成功,那么设置当前节点为 head
,并将原 head
的 next
设置为 null
3.3. 如果 3.2 步骤不满足。根据节点 waitStatus
的值,做不同的处理
1. 若 `waitStatus = SIGNAL` 直接返回 true,并执行 `park`,返回中断标记
2. 若 `waitStatus = CANCELLED` 将取消节点从队列中删除, 返回 `false`
3. 若 `waitStatus` 为其他值,设置 `waitStatus = SIGNAL`, 返回 `false`
完整流程入图所示
1.3 几个关于源码疑惑的解答
Q: 为什么入队列需要用当前 node
的 prev
先指向 tail
, 再用 tail
的 next
指向 node
,
A: 如果先用 tail
的 next
指向 node
, 那么当 node
CAS 设置为 tail
成功之后,但是 tail
的 next
指针却可能指向别的 node
2、出同步队列
tryRelease
失败,直接返回head == null
, 或waitStatus == 0
,返回 成功- 如果
waitStatus
值小于 0,需先设置为 0 - 获取
head
的下一个节点next
。如果next
为null,或者waitStatus > 0
, 则需要从tail
节点向前遍历,找到waitStatus <= 0
且距离head
节点最近的节点,并将之唤醒
注:出队列,本质是 unpark head
的 next
节点。而节点出队列的动作,实际上是在 acquireQueued 中处理。
注:注意看同步队列的数据结构,head 节点一直是一个不指向任何线程的 Node 节点,可以理解为哨兵节点
完整流程入图所示
3、入等待队列
3.1、等待队列数据结构
等待队列是一个单向的链表队列,结构如下
3.2、入队列流程
线程可以调用 await
方法,说明线程已经拿到了锁。因此 await
的流程,相对简单
3.2.1 无参的 await
- 将当前线程构造成
Node
节点,加入等待队列中 - 调用
release
(出同步队列) 释放当前占用的锁资源 - 如果线程处于等待队列中,执行
park
;否则退出循环 - 因为是无参
await
,当前线程需要被唤醒,才能继续执行。 - 线程被唤醒后,如果不是因为中断唤醒,那么会继续重复 3 - 5 动作。
- 线程被唤醒,并且已经处于同步队列中,尝试获取锁
完整流程入图所示
3.2.2 带时间的 await
- 将当前线程构造成
Node
节点,加入等待队列中 - 调用
release
(出同步队列) 释放当前占用的锁资源 - 如果线程处于等待队列中(判断的条件即:
awaitStatus = CANDITION
)执行park
;否则退出循环 - 如果等待超时,则入同步队列,退出循环
- 如果等待时间大于自旋时间(1m),则
park
。 - 重复 3 - 5 动作
- 线程被唤醒,并且已经处于同步队列中,尝试获取锁
完整流程图如下
3.3 几个关于源码疑惑的解答
Q: 为什么得判断节点不处于同步队列才需要 park
?
A: 从源码可以看出,线程是先入的等待队列,然后才释放资源。也就是在释放锁资源后,有可能其他的线程,抢到了锁,调用了 signal
,并且把先前的线程唤醒,并插入到同步队列中。
Q: 为什么 await
时间小于等于 1秒 时,只需自旋,不需要 park
A: 若当前没有任何许可,park
会将当前线程挂起,发生线程切换,效率会比自旋差。
4、出等待队列
- 将
firstWaiter
移出等待队列中,并设置新的firstWaiter
CAS
将firstWaiter
的waitStatus
值设置为SINGLE
(期待CONDITION
)- CAS 设置成功,则将
firstWaiter
节点入同步队列 - 如果同步队列 原尾节点 被取消,或者节点的状态被改变了,则将原尾节点唤醒
流程图如下
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。