重学 JAVA -- AQS 终

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 将当前节点设置为尾节点, 最后再将 tailnext 指针指向当前节点
    2. 如果尝试快速插入失败,则会使用自旋的方式,执行上诉步骤直到成功
  3. 节点自旋获取锁(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. 线程被唤醒,并且已经处于同步队列中,尝试获取锁

完整流程图如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MMiIyrAb-1603376915524)(evernotecid://B58292A4-43F3-4C3E-ACA3-E9C7D829CC18/appyinxiangcom/15559189/ENResource/p3050)]

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
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
AQS(AbstractQueuedSynchronizer)是Java并发编程中的一个重要类,它可以理解为抽象的队列同步器。AQS提供了一种基于FIFO队列的同步机制,用于实现各种同步器,如ReentrantLock、CountDownLatch、Semaphore等。 AQS的核心思想是使用一个volatile的int类型变量state来表示同步状态,通过CAS(Compare and Swap)操作来实现对state的原子更新。AQS内部维护了一个双向链表,用于保存等待获取同步状态的线程。 AQS的具体实现包括以下几个方面: 1. 内部属性:AQS内部有两个重要的属性,一个是head,表示队列的头节点;另一个是tail,表示队列的尾节点。 2. 入队操作:AQS的入队操作是通过enq方法实现的。在入队操作中,首先判断队列是否为空,如果为空,则需要初始化队列;否则,将新节点添加到队列的尾部,并更新tail指针。 3. CAS操作:AQS的CAS操作是通过compareAndSetHead和compareAndSetTail方法实现的。这些方法使用CAS操作来更新head和tail指针,保证操作的原子性。 4. 出队操作:AQS的出队操作是通过deq方法实现的。在出队操作中,首先判断队列是否为空,如果为空,则返回null;否则,将头节点出队,并更新head指针。 5. 同步状态的获取和释放:AQS提供了acquire和release方法来获取和释放同步状态。acquire方法用于获取同步状态,如果获取失败,则会将当前线程加入到等待队列中;release方法用于释放同步状态,并唤醒等待队列中的线程。 通过继承AQS类,可以实现自定义的同步器。具体的实现方式是重写AQS的几个关键方法,如tryAcquire、tryRelease等,来实现对同步状态的获取和释放。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值