1、入同步队列

AQS 在尝试获取锁失败时,会将当前线程构造成 Node 节点,插入同步队列中。
在说入队列操作之前,需要对 Node 的数据结构进行一下说明

1.1、同步队列 数据结构

同步队列 的数据结构为 双向链表

NodewaitStatus 值的定义非常关键。

// 线程已取消
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;

image.png

1.2、入队列流程

  1. tryAcquire 失败后,线程会被构造为 Node 节点加入到同步队列。
  2. 入队列(addWaiter)

2.1. 如果 tail == null
构造一个不指向任何线程的 Node 节点,并设置为 head,tail
2.2. 如果 tail != null

1. 尝试快速插入:尝试将当前节点设置为尾节点,如果成功则直接返回。这里的指针操作细节为:先将当前节点 `prev` 指针指向 `tail`, 再用 `CAS` 将当前节点设置为尾节点, 最后再将 `tail` 的 `next` 指针指向当前节点
2. 如果尝试快速插入失败,则会使用自旋的方式,执行上诉步骤直到成功      
  1. 节点自旋获取锁(acquireQueued)

3.1. 获取当前节点的上一个节点 prev
3.2. 如果 prev 为头节点,且 tryAcquire 成功,那么设置当前节点为 head,并将原 headnext 设置为 null
3.3. 如果 3.2 步骤不满足。根据节点 waitStatus 的值,做不同的处理

1. 若 `waitStatus = SIGNAL` 直接返回 true,并执行 `park`,返回中断标记
2. 若 `waitStatus = CANCELLED` 将取消节点从队列中删除, 返回 `false`
3. 若 `waitStatus` 为其他值,设置 `waitStatus = SIGNAL`, 返回 `false`

完整流程入图所示
image.png

1.3 几个关于源码疑惑的解答

Q: 为什么入队列需要用当前 nodeprev 先指向 tail, 再用 tailnext 指向 node,
A: 如果先用 tailnext 指向 node, 那么当 node CAS 设置为 tail 成功之后,但是 tailnext 指针却可能指向别的 node

2、出同步队列

  1. tryRelease 失败,直接返回
  2. head == null, 或 waitStatus == 0,返回 成功
  3. 如果 waitStatus 值小于 0,需先设置为 0
  4. 获取 head 的下一个节点 next。如果 next 为null,或者waitStatus > 0, 则需要从 tail 节点向前遍历,找到 waitStatus <= 0 且距离 head 节点最近的节点,并将之唤醒

注:出队列,本质是 unpark headnext 节点。而节点出队列的动作,实际上是在 acquireQueued 中处理。
注:注意看同步队列的数据结构,head 节点一直是一个不指向任何线程的 Node 节点,可以理解为哨兵节点

完整流程入图所示
image.png

3、入等待队列

3.1、等待队列数据结构

等待队列是一个单向的链表队列,结构如下
image.png

3.2、入队列流程

线程可以调用 await 方法,说明线程已经拿到了锁。因此 await 的流程,相对简单

3.2.1 无参的 await
  1. 将当前线程构造成 Node 节点,加入等待队列中
  2. 调用 release(出同步队列) 释放当前占用的锁资源
  3. 如果线程处于等待队列中,执行 park;否则退出循环
  4. 因为是无参 await,当前线程需要被唤醒,才能继续执行。
  5. 线程被唤醒后,如果不是因为中断唤醒,那么会继续重复 3 - 5 动作。
  6. 线程被唤醒,并且已经处于同步队列中,尝试获取锁

完整流程入图所示
image.png

3.2.2 带时间的 await
  1. 将当前线程构造成 Node 节点,加入等待队列中
  2. 调用 release(出同步队列) 释放当前占用的锁资源
  3. 如果线程处于等待队列中(判断的条件即:awaitStatus = CANDITION)执行 park;否则退出循环
  4. 如果等待超时,则入同步队列,退出循环
  5. 如果等待时间大于自旋时间(1m),则 park
  6. 重复 3 - 5 动作
  7. 线程被唤醒,并且已经处于同步队列中,尝试获取锁

完整流程图如下
image.png

3.3 几个关于源码疑惑的解答

Q: 为什么得判断节点不处于同步队列才需要 park
A: 从源码可以看出,线程是先入的等待队列,然后才释放资源。也就是在释放锁资源后,有可能其他的线程,抢到了锁,调用了 signal,并且把先前的线程唤醒,并插入到同步队列中。
Q: 为什么 await 时间小于等于 1秒 时,只需自旋,不需要 park
A: 若当前没有任何许可,park 会将当前线程挂起,发生线程切换,效率会比自旋差。

4、出等待队列

  1. firstWaiter 移出等待队列中,并设置新的 firstWaiter
  2. CASfirstWaiterwaitStatus 值设置为 SINGLE(期待 CONDITION)
  3. CAS 设置成功,则将 firstWaiter 节点入同步队列
  4. 如果同步队列 原尾节点 被取消,或者节点的状态被改变了,则将原尾节点唤醒

流程图如下
image.png


心无私天地宽
513 声望22 粉丝