共计 2090 个字符,预计需要花费 6 分钟才能阅读完成。
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
节点入同步队列 - 如果同步队列 原尾节点 被勾销,或者节点的状态被扭转了,则将原尾节点唤醒
流程图如下