关于java:AQS-终

74次阅读

共计 2090 个字符,预计需要花费 6 分钟才能阅读完成。

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;

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`

残缺流程入图所示

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 节点,能够了解为哨兵节点

残缺流程入图所示

3、入期待队列

3.1、期待队列数据结构

期待队列是一个单向的链表队列,构造如下

3.2、入队列流程

线程能够调用 await 办法,阐明线程曾经拿到了锁。因而 await 的流程,绝对简略

3.2.1 无参的 await
  1. 将以后线程结构成 Node 节点,退出期待队列中
  2. 调用 release(出同步队列)开释以后占用的锁资源
  3. 如果线程处于期待队列中,执行 park;否则退出循环
  4. 因为是无参 await,以后线程须要被唤醒,能力继续执行。
  5. 线程被唤醒后,如果不是因为中断唤醒,那么会持续反复 3 – 5 动作。
  6. 线程被唤醒,并且曾经处于同步队列中,尝试获取锁

残缺流程入图所示

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

残缺流程图如下

3.3 几个对于源码纳闷的解答

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

4、出期待队列

  1. firstWaiter 移出期待队列中, 并设置新的 firstWaiter
  2. CASfirstWaiterwaitStatus 值设置为 SINGLE(期待 CONDITION)
  3. CAS 设置胜利,则将 firstWaiter 节点入同步队列
  4. 如果同步队列 原尾节点 被勾销,或者节点的状态被扭转了,则将原尾节点唤醒

流程图如下

正文完
 0