AQS 抽象队列同步器

AQS

AQS (抽象队列同步器): AbstractQueuedSynchronizer 是什么

  • 来自jdk1.5,是用来实现锁或者其他同步器组件的公共基础部分的抽象实现,是重量级基础框架以及JUC的基石,主要用于解决锁分配给谁的问题
  • 整体是通过一个抽象的FIFO队列来完成资源获取线程的排队工作,并通过一个int变量表示持有锁的状态
  • 锁是面向开发人员的,而同步器是JDK统一规范并简化了锁的实现,并抽象出来的公共基础部分,屏蔽了同步状态、同步队列的管理,和线程排队、通知、唤醒等机制

和AQS相关的类有:

  • ReentranLock
  • CountDownLatch
  • ReentrantReadWriteLock
  • Semaphore

在这里插入图片描述

AQS 原理

  • 整体是通过一个抽象的FIFO队列来完成资源获取线程的排队工作,并通过一个int变量表示持有锁的状态
  • 如果共享资源被占用,就需要阻塞唤醒机制来保证锁的分配,这个机制主要是通过CLH队列的变体实现的,将暂时获取锁失败的线程,以及自身的等待状态封装成队列的节点对象node,放入队列中
  • 通过CAS、自旋等维护共享资源的状态,达到并发效果
  • 内部结构:
    • 队列的 头指针、尾指针
    • int 类型的同步状态的标识 state ,默认值0代表没有被占用,大于等于1代表被占用
    • 内部类node,将暂时获取锁失败的线程,以及自身的等待状态封装成队列的节点对象node
      • int类型变量 waitStatus 当前节点再队列中的等待状态,默认为0
        • 1表示线程被取消
        • -1表示后继线程需要被唤醒
        • -2表示等待conditon唤醒
        • -3表示共享式(锁分为共享和独占)同步状态获取将无条件地传播下去
      • 前一个节点的指针和后一个节点的指针
      • 请求线程

CLH队列

  • (Craig, Landin,Hagersten) 三个科学家名字的简称
  • 通过state状态判断是否阻塞,从尾部入队,头部出队

以ReentrantLock为例,加锁和解锁的步骤为:

lock
  • 当调用 lock 方法加锁时

    • 非公平锁,会先尝试通过cas 比较并交换的操作把 states 的状态值从 0更新为1,如果更新成功,就把持有锁的线程设置为自己
    • 更新失败就和公平锁一样,执行 AQS 的 acquire方法
    • 除此之外,公平和非公平锁的区别就是,再获取同步状态时,公平锁需要判断等待队列中再自己之前是否存在有效节点,如果有公平锁就需要排队
    • 因为公平锁讲究先到先得,线程再获取锁时,如果这个锁的等待队列已经有线程再等待,当前线程就会直接进入等待队列
    • 而非公平锁,不管是否有队列,如果可以获取锁,就会立刻占有锁的对象,所以第一个在队列里排队的线程苏醒后,仍然需要去竞争锁,且不一定能竞争到锁

acquire 方法源码:

    public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}

acquire

  • 调用lock方法加锁,除非是非公平锁能直接拿到锁,其他情况下都是在调用acquire 方法
  • acquire 方法分为三种情况:
  • 调用 tryAcquire 方法尝试加锁;
    • AQS类的tryAcquire方法只是做了规范,方法内直接抛出异常,所以这个方法需要由子类去实现

    • 非公平锁的tryAcquire 方法会先判断锁的状态state是否为0,为0说明没有被其他线程占用,就立即使用cas操作变更state为1,变更成功就把持有锁的线程设置为自己,变更失败就表示加锁失败

    • 如果锁的状态为1,说明锁已经被占用,在比较当前线程和持有锁的线程是否一致,不一致就加锁失败

    • tryAcquire 方法公平和非公平锁的区别是

      • 再获取同步状态时,公平锁需要判断等待队列中再自己之前是否存在有效节点,如果有公平锁就需要排队
      • 非公平锁,不管是否有队列,如果可以获取锁,就会立刻占有锁的对象
    • 如果 tryAcquire 方法抢锁失败,就需要调用 addWaiter加入到等待队列

  • 加锁失败,调用 addWaite方法,进入等待队列;
    • acquire 方法的 addWaiter 方法创建的是独占的node节点,节点中封装的是当前线程
    • 首先要判断 链表的尾指针是否为空
      • 如果为空,就需要初始化链表,首先new一个空的哨兵节点,这个节点并不存储信息,只是作为占位使用,然后设置哨兵节点为头节点,然后把头节点赋值给尾节点
      • 当链表初始化完成后,或者链表中已经由其他节点时,就用CAS操作把新节点加入到链表尾部,如果节点加入链表失败就进行下一次循环,直到把节点加入成功为止
      • 如果不为空,直接用CAS操作把新节点加入到链表尾部,同样如果节点加入链表失败就进行循环,直到把节点加入成功为止
    • 节点成功入队后,需要调用acquireQueued 方法
  • 进入队列之后,调用acquireQueued 方法,线程进入阻塞状态,等待唤醒后才能继续执行
    • 首先获取当前节点的前置节点,如果前置节点是头节点,就尝试去获取锁
    • 如果获取锁成功,就把自己设为头节点,就把锁的state改为1,设置当前线程为持有锁的线程
    • 如果前置节点不是头节点,或者获取锁失败
      • 就需要判断前置节点的waitStatus状态值,waitStatus值默认为0,第一次进入循环,会把前置节点的waitStatus的值改为-1后,继续下一次循环后,会调用 LockSupport.park 方法阻塞当前线程,需要等待其他线程释放锁后,再唤醒阻塞的线程
      • 当持有锁的线程释放锁,且调用LockSupport.unpark 唤醒该线程后才能继续执行,LockSupport.unpark 唤醒的是头节点的下一个节点
      • 线程被唤醒后,检查线程是否被中断,如果线程没有被中断,就继续进行循环
      • 继续尝试去加锁,因为是非公平锁,所以有可能会加锁失败
        • 如果加锁成功,就把锁的state改为1,设置当前线程为持有锁的线程,并且把当前线程的节点设置为链表的头节点,原本的头节点会从链表中剔除
        • 因为每次唤醒的都是头节点的下一个节点,所以成功抢到到锁后,被唤醒的节点会成为新的头节点,后续会唤醒链表的下一个节点
    • 如果线程在等待过程中取消,没有获取到锁就跳出了循环,failed值为默认的true,就会执行cancelAcquire方法,取消正在排队的节点
      • 首先设置当前节点的线程为null,然后获取上一个没有取消的前置节点,
      • 把当前节点的 waitStatus 设置为1(1就是要取消的节点)
      • 如果当前节点是尾节点,就把上一个有效的节点设置为尾节点
      • 如果不是尾节点,并且满足出队条件,就变更链表中相关节点的前置和后置引用,剔除要取消的节点

非公平锁的 tryAcquire 方法源码

        final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();//先判断锁的状态state是否为0,为0说明没有被其他线程占用if (c == 0) {//为0说明没有被其他线程占用,使用cas操作变更state为1if (compareAndSetState(0, acquires)) {//变更成功就把持有锁的线程设置为自己,变更失败就表示加锁失败setExclusiveOwnerThread(current);return true;}}//如果锁的状态为1,说明锁已经被占用,在比较当前线程和持有锁的线程是否一致else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}//加锁失败return false;}

addWaiter(Node.EXCLUSIVE) 加入等待队列 源码:

  • Node.EXCLUSIVE 代表的是独占的节点,也就是排他锁
private Node addWaiter(Node mode) {//node节点中封装的是当前线程Node node = new Node(Thread.currentThread(), mode);//尾指针Node pred = tail;//链表的尾指针是否为空if (pred != null) {node.prev = pred;//如果加入失败,就会走下面的循环,直到把节点加入链表为止if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}enq(node);return node;
}private Node enq(final Node node) {for (;;) {//尾节点Node t = tail;//如果尾节点为nullif (t == null) { // Must initialize//就需要new一个node节点,并且设置为头节点,然后把头节点赋值给尾节点if (compareAndSetHead(new Node()))tail = head;} else {//当链表初始化完成后,或者链表中已经由其他节点时//把要加入链表的新节点的前指针设置为尾节点node.prev = t;//并且把新加入的节点设置为尾节点if (compareAndSetTail(t, node)) {//设置成功就之前尾节点的后指针指向新节点,这样新节点就变成了新的尾节点,如果设置失败,就继续循环,直到把新节点加入到链表尾部为止t.next = node;return t;}}}}

acquireQueued 源码

  • 线程进入阻塞状态,等待唤醒后才能继续执行
	//arg为1,独占锁final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {//获得node节点的前置节点final Node p = node.predecessor();//node节点的前置节点是否为头节点,如果是就尝试去获取锁if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return interrupted;}//判断node节点的前置节点的waitStatus状态,默认情况下都是0,在第二次循环的时候,就会改成-1,然后执行parkAndCheckInterrupt方法//parkAndCheckInterrupt方法会阻塞当前线程//也就是后面的节点会把前面节点的 waitStatus 改为-1if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}}/*** waitStatus 当前节点再队列中的等待状态默认为01表示线程获取锁的请求被取消-1表示线程已经准备好了-2表示节点在等待队列中,等待唤醒-3表示共享式(锁分为共享和独占)同步状态获取将无条件地传播下去*/private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {//前置节点的状态int ws = pred.waitStatus;// SIGNAL= -1 , 当线程再次进行循环的时候,前一个节点的waitStatus已经被设置为-1,就返回trueif (ws == Node.SIGNAL)return true;//线程被取消if (ws > 0) {do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {//如果前置节点的 waitStatus不等于-1也不大于0,就把waitStatus的值改为-1后,返回falsecompareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;}//阻塞当前线程private final boolean parkAndCheckInterrupt() {//验证当前线程的通行证,阻塞当前线程LockSupport.park(this);//被唤醒后,检查线程是否被中断,如果线程没有被中断,就返回 falsereturn Thread.interrupted();}

取消正在进行的获取尝试

	// node 为需要取消的节点private void cancelAcquire(Node node) {// Ignore if node doesn't existif (node == null)return;//设置当前节点的线程为nullnode.thread = null;//获取上一个节点Node pred = node.prev;//waitStatus > 0 ,表示上一个节点也要取消while (pred.waitStatus > 0)//那么就一直向上找,直到找到没有取消的前置节点node.prev = pred = pred.prev;//获取不会取消的前置节点的下一个节点Node predNext = pred.next;//把当前节点的 waitStatus 设置为1,1就是要取消的节点node.waitStatus = Node.CANCELLED;//如果当前节点是尾节点,就把上一个还有效的节点设置为尾节点if (node == tail && compareAndSetTail(node, pred)) {//设置成功,就把上一个节点的后置节点设置为null,这样上一个还有效的节点就成为了尾节点compareAndSetNext(pred, predNext, null);} else {//否则int ws;//前置节点不能是头节点,因为头节点只是占位节点,并且满足出队条件if (pred != head &&((ws = pred.waitStatus) == Node.SIGNAL ||(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&pred.thread != null) {//变更链表中相关节点的前置和后置引用,剔除要取消的节点Node next = node.next;if (next != null && next.waitStatus <= 0)compareAndSetNext(pred, predNext, next);} else {unparkSuccessor(node);}node.next = node; // help GC}}
unlock

unlock 源码,其实是再调用release方法

    public void unlock() {sync.release(1);}

release方法会首先尝试释放锁

  • tryRelease 会把持有锁的线程为null,并且把锁的state设置为0
  • 如果链表被初始化过,有在等待的线程节点,头节点就不为空,且waitStatus值为-1
  • 接下来会把头节点的waitStatus的改为0,如果头节点的下一个节点不为null,就调用LockSupport.unpark 方法,唤醒头节点的下一个节点
    public final boolean release(int arg) {if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;}

AQS的tryRelease方法,同样没有做实现,需要子类自己去实现,下面是ReentrantLock的实现

		protected final boolean tryRelease(int releases) {//传入的releases为1,持有锁的线程State为1,所以C为0int c = getState() - releases;//如果当前线程不等于持有锁的线程会抛出异常,这种情况一般不会出现if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;//c等于0,就设置持有锁的线程为null,并且把state设置为0,返回trueif (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;}

unparkSuccessor

    private void unparkSuccessor(Node node) {int ws = node.waitStatus;if (ws < 0)//重新把头节点的waitStatus值改为0compareAndSetWaitStatus(node, ws, 0);//头节点的下一个节点Node s = node.next;//如果链表被初始化过,有在等待的线程节点,头节点的后置节点就不为null//如果链表后面还有其他节点,那么头节点的后置节点waitStatus值就为-1if (s == null || s.waitStatus > 0) {s = null;for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}//如果头节点的下一个节点不为null,就直接调用 LockSupport.unpark 方法,唤醒头节点的下一个节点if (s != null)LockSupport.unpark(s.thread);}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/232677.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

互联网演进历程:从“全球等待”到“全球智慧”的技术革新与商业变革

文章目录 一、导言二、World Wide Wait (全球等待)阶段1. 技术角度2. 用户体验3. 企业收益4. 教育影响 三、World Wide Web (万维网)阶段1. 技术角度2. 用户体验3. 企业收益4. 教育影响 四、World Wide Wisdom (全球智慧)阶段1. 技术角度2. 用户体验3. 企业收益4. 教育影响 五、…

MySQL——用户管理

目录 一.用户管理 二.用户 1.用户信息 2.创建用户 3.删除用户 4. 修改用户密码 三.数据库的权限 1.给用户授权 2.回收权限 一.用户管理 如果我们只能使用root用户&#xff0c;root的权限非常大,这样存在安全隐患。这时&#xff0c;就需要使用MySQL的用户管理&#xff…

苦学golang半年,写了一款web服务器

苦学golang半年&#xff0c;写了一款web服务器 文章目录 苦学golang半年&#xff0c;写了一款web服务器example 项目地址&#xff1a;https://github.com/fengyuan-liang/jet-web-fasthttp 可以的话&#xff0c;请star支持一下&#x1f642; 苦学golang半年&#xff0c;写了一款…

java基于vue的音乐播放器的设计与实现论文

摘 要 当下&#xff0c;如果还依然使用纸质文档来记录并且管理相关信息&#xff0c;可能会出现很多问题&#xff0c;比如原始文件的丢失&#xff0c;因为采用纸质文档&#xff0c;很容易受潮或者怕火&#xff0c;不容易备份&#xff0c;需要花费大量的人员和资金来管理用纸质文…

【Spring Cloud】Feign组件的使用及参数的传递

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是Java方文山&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;推荐给大家我的专栏《Spring Cloud》。&#x1f3af;&#x1f3af; &am…

浅析观察者模式在Java中的应用

观察者模式&#xff08;Observer Design Pattern&#xff09;,也叫做发布订阅模式&#xff08;Publish-Subscribe Design Pattern&#xff09;、模型-视图&#xff08;Model-View&#xff09;模式、源-监听器&#xff08;Source-Listener&#xff09;模式、从属者&#xff08;D…

强化学习5——动态规划在强化学习中的应用

动态规划在强化学习中的应用 基于动态规划的算法优良 &#xff1a;策略迭代和价值迭代。 策略迭代分为策略评估和策略提升&#xff0c;使用贝尔曼期望方程得到一个策略的状态价值函数&#xff1b;价值迭代直接使用贝尔曼最优方程进行动态规划&#xff0c;得到最终的最优状态价…

【SpringCloud】之配置中心(进阶使用)

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是君易--鑨&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;推荐给大家我的博客专栏《SpringCloud开发之远程消费》。&#x1f3af;&a…

5年经验之谈 —— 探索自动化测试用例设计粒度!

自动化测试用例的粒度指的是测试用例的细致程度&#xff0c;即每个测试用例检查的功能点的数量和范围。 通常&#xff0c;根据测试用例的粒度&#xff0c;可以被分为3种不同的层次&#xff0c;从更低层次的细粒度到更高层次的粗粒度。 第一种&#xff1a;单元测试 - 细粒度 单…

node:全局对象事件环buffer

node&#xff1a;全局对象&事件环&buffer 全局对象 exports/module/require/__dirname/__filename&#xff1a;这些是参数 global全局对象&#xff0c;挂载global上的 process process 进程&#xff0c;代码node服务都是跑在一个进程里面。进程和集群 process上常用属性…

muduo网络库剖析——网络地址InetAddress类

muduo网络库剖析——网络地址InetAddress类 前情从muduo到my_muduo 概要socketaddr_in介绍成员用法 网络地址转换函数 框架与细节成员函数使用方法 源码 前情 从muduo到my_muduo 作为一个宏大的、功能健全的muduo库&#xff0c;考虑的肯定是众多情况是否可以高效满足&#xf…

rime中州韵小狼毫 help lua Translator 帮助消息翻译器

lua 是 Rime中州韵/小狼毫输入法强大的武器&#xff0c;掌握如何在Rime中州韵/小狼毫中使用lua&#xff0c;你将体验到什么叫 随心所欲。 先看效果 在 rime中州韵 输入效果一览 中的 &#x1f447; help效果 一节中&#xff0c; 我们看到了在Rime中州韵/小狼毫输入法中输入 h…

Mediant approximation trick

近似值的一个取值技巧 如果知道一个数值变量的上限和下限&#xff0c;那么有一种快速的方法&#xff0c;快速获取该变量更准确的近似值。 比如&#xff0c;已知变量e的大小范围是19/7 < e < 87/32&#xff0c;就可以快速得到它的近似值。 Suppose you are trying to ap…

Navicat 技术干货 | 如何查看关系型数据库(MySQL、PostgreSQL、SQL Server、 Oracle)查询的运行时间

在数据库优化中&#xff0c;理解和监控查询运行时间是至关重要的。无论你是数据库管理员、开发人员或是参与性能调优的人员&#xff0c;知道如何查看查询运行时间能为你的数据库操作提供有价值的参考。本文中&#xff0c;我们将探索几款热门的关系数据库&#xff08;如 MySQL、…

大模型实战营Day1 书生·浦语大模型全链路开源体系

1.大模型为发展通用人工智能的重要途经 专用模型&#xff1a;针对特定任务解决特定问题 通用大模型&#xff1a;一个模型对应多模态多任务 2.InternLM大模型开源历程 3.InternLM-20B大模型性能 4.从模型到应用&#xff1a;智能客服、个人助手、行业应用 5.书生浦语全链条开源…

20240106-换一种思维,工作也不过就是一种挣钱的方式而已了

今天在车上一个百度的同事聊抱怨说&#xff1a;累了&#xff0c;真的累了&#xff0c;干不动了&#xff0c;想跑路了&#xff0c;不想打工了。我们之前也会经常聊到和吐槽这种事情&#xff0c;但是我最近由于思维的一些改变&#xff0c;所以就想到把这个事情记录下来。 在大厂…

vue-springboot基于JAVA的小碗菜外卖套餐订单系统的设计与实现9r2r3

想要使用这个平台进行购买物品或服务的人具体的功能需求分为注册登录、餐品购买&#xff0c;餐品搜索&#xff0c;购物车&#xff0c;个人中心&#xff0c;查看已购买过的餐品&#xff0c;餐品评价。具体功能模块描述&#xff1a; &#xff08;1&#xff09;注册登录 想要使用这…

[MAUI]在.NET MAUI中调用拨号界面

在.NET MAUI中调用拨号界面 前置要求: Visual Studio 2022 安装包“.NET Multi-platform App UI 开发” 参考文档: 电话拨号程序 新建一个MAUI项目 在解决方案资源管理器窗口中找到Platforms/Android/AndroidManifest.xml在AndroidManifest.xml中添加下文中…块如下:<?xml…

【操作系统xv6】学习记录5--实验1 Lab: Xv6 and Unix utilities

ref:https://pdos.csail.mit.edu/6.828/2020/xv6.html 实验&#xff1a;Lab: Xv6 and Unix utilities 环境搭建 实验环境搭建&#xff1a;https://blog.csdn.net/qq_45512097/article/details/126741793 搭建了1天&#xff0c;大家自求多福吧&#xff0c;哎。~搞环境真是折磨…

MySQL第四战:视图以及常见面试题(上)

目录 目录&#xff1a; 一.视图 1.介绍什么是视图 2.视图的语法 语法讲解 实例操作 二.MySQL面试题 1.SQL脚本 2.面试题实战 三.思维导图 目录&#xff1a; 随着数字化时代的飞速发展&#xff0c;数据库技术&#xff0c;特别是MySQL&#xff0c;已经成为IT领域中不可…