Java技术之AQS详解

2023-11-03

在这里插入图片描述

AbstractQueuedSynchronizer

简写为AQS,抽象队列同步器。它是一个用于构建锁和同步器的框架,许多同步器都可以通过AQS很容易并且高效的构造出来,以下都是通过AQS构造出来的:ReentrantLock, ReentrantReadWriteLock

AQS使用了模板方法,把同步队列都封装起来了,同时提供了以下五个未实现的方法,用于子类的重写:

  • boolean tryAcquire(int arg):尝试以独占模式进行获取。
    此方法应查询对象的状态是否允许以独占模式获取对象,如果允许则获取它。如果获取失败,则将当前线程加入到等待队列,直到其他线程唤醒。
  • boolean tryRelease(int arg):尝试以独占模式释放锁。
  • int tryAcquireShared(int
    arg):尝试以共享模式获取锁,此方法应查询对象的状态是否允许以共享模式获取对象,如果允许则获取它。如果获取失败,则将当期线程加入到等待队列,直到其他线程唤醒。
  • boolean tryReleaseShared(int arg):尝试以共享模式释放锁。
  • boolean isHeldExclusively():是否独占模式
    在这里插入图片描述
  • state:所有线程通过通过CAS尝试给state设值,当state>0时表示被线程占用;同一个线程多次获取state,会叠加state的值,从而实现了可重入;
  • exclusiveOwnerThread:在独占模式下该属性会用到,当线程尝试以独占模式成功给state设值,该线程会把自己设置到exclusiveOwnerThread变量中,表明当前的state被当前线程独占了;
  • 等待队列(同步队列):等待队列中存放了所有争夺state失败的线程,是一个双向链表结构。state被某一个线程占用之后,其他线程会进入等待队列;一旦state被释放(state=0),则释放state的线程会唤醒等待队列中的线程继续尝试cas设值state;
  • head:指向等待队列的头节点,延迟初始化,除了初始化之外,只能通过setHead方法进行修改;
  • tail:指向等待队列的队尾,延迟初始化,只能通过enq方法修改tail,该方法主要是往队列后面添加等待节点。

AQS队列节点数据结构

在这里插入图片描述

AQS中的一般处理流程

1.public final void acquire(int arg)
这个方法是使用独占模式获取锁,忽略中断。通过至少调用一次tryAcquire成功返回来实现。 否则,线程将排队,并可能反复阻塞和解除阻塞,并调用tryAcquire直到成功。

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    public final void acquire(int arg) {
        // 尝试获取锁,这里是一个在AQS中未实现的方法,具体由子类实现
        if (!tryAcquire(arg) &&  
        
            // 获取不到锁,则 1.添加到等待队列 2.不断循环等待重试
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))  
            selfInterrupt();
    }
}

2.tryAcquire(int arg)

— 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。 非公平锁在CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state== 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。

公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。

相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。

一开始,会尝试调用AQS中未实现的方法tryAcquire()尝试获取锁,获取成功则表示获取锁了,该方法的实现一般通过CAS进行设置state尝试获取锁:
在这里插入图片描述
不同的锁可以有不同的tryAcquire()实现,所以,你可以看到ReentrantLock锁里面会有非公平锁和公平锁的实现方式。

ReentrantLock公平锁的实现代码在获取锁之前多了一个判断:!hasQueuedPredecessors(),这个是判断如果当前线程节点之前没有其他节点了,那么我们才可以尝试获取锁,这就是公平锁的体现。

3. private Node addWaiter(Node mode)
获取锁失败之后,则会进入这一步,这里会尝试把线程节点追加到等待队列后面,是通过CAS进行追加的,追加失败的情况下,会循环重试,直至追加成功为止。如果追加的时候,发现head节点还不存在,则先初始化一个head节点,然后追加上去:

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    private Node addWaiter(Node mode) {
        // 将当期线程构造成Node节点
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            // 将原来尾节点设置为新节点的上一个节点
            node.prev = pred;
            // 尝试用新节点取代原来的尾节点
            if (compareAndSetTail(pred, node)) {
                // 取代成功,则将原来尾指针的下一个节点指向新节点
                pred.next = node;
                return node;
            }
        }
        // 如果当前尾指针为空,则调用enq方法
        enq(node);
        return node;
    }
}

4.final boolean acquireQueued(final Node node, int arg)
加入等待队列之后,会执行该方法,不断循环地判断当前线程节点是否在head后面一位,如果是则调用tryAcquire()获取锁,如果获取成功,则把线程节点作为Node head,并把原Node head的next设置为空,断开原来的Node head。注意这个Node head只是占位作用,每次处理的都是Node head的下一个节点:

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    /**
     * 已经入队的线程尝试获取锁
     */ 
    final boolean acquireQueued(final Node node, int arg) {
        //标记是否成功获取锁
        boolean failed = true;
        try {
            //标记线程是否被中断过
            boolean interrupted = false;
            for (;;) {
                //获取前驱节点
                final Node p = node.predecessor();
                //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取锁
                if (p == head && tryAcquire(arg)) {
                    // 获取成功,将当前节点设置为head节点
                    setHead(node);
                    // 原head节点出队,在某个时间点被GC回收
                    p.next = null; // help GC
                    //获取成功
                    failed = false;
                    //返回是否被中断过
                    return interrupted;
                }
                // 判断是否需要阻塞线程,该方法中会把取消状态的节点移除掉,并且把当前节点的前一个节点设置为SIGNAL
                // 判断获取失败后是否可以挂起,若可以则挂起
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    // 线程若被中断,设置interrupted为true
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
}

如果当前节点的pre不是head,或者争抢失败,则会将前面节点的状态设置为SIGNAL。
如果前面的节点状态大于0,表示节点被取消,这个时候会把该节点从队列中移除掉。
下图为尝试CAS争抢锁,但失败了,然后把head节点状态设置为SIGNAL:
在这里插入图片描述
然后再会循环一次尝试获取锁,如果获取失败了,就调用LockSupport.park(this)挂起线程。

那么时候才会触发唤起线程呢?这个时候我们得先看看释放锁是怎么做的了。
5.public final boolean release(int arg)

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    public final boolean release(int arg) {
        //如果成功释放锁
        if (tryRelease(arg)) {
            //获取头节点:(注意:这里的头节点就是当前正在释放锁的节点)
            Node h = head;
            //头结点存在且等待状态不是取消
            if (h != null && h.waitStatus != 0)
                //唤醒距离头节点最近的一个非取消的节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
}

tryRelease()具体由子类实现。一般处理流程是让state减1。

如果释放锁成功,并且头节点waitStatus!=0,那么会调用unparkSuccessor()通知唤醒后续的线程节点进行处理。

注意:在遍历队列查找唤醒下一个节点的过程中,如果发现下一个节点状态是CANCELLED那么就会忽略这个节点,然后从队列尾部向前遍历,找到与头结点最近的没有被取消的节点进行唤醒操作。
在这里插入图片描述
唤醒之后,节点对应的线程2又从acquireQueued()方法的阻塞处醒来继续参与争抢锁。并且争抢成功了,那么会把head节点的下一个节点设置为null,让自己所处的节点变为head节点:
在这里插入图片描述
这样一个AQS独占式、非中断的抢占锁的流程就结束了。
6.完整流程
最后我们再以另一个维度的流程来演示下这个过程。

首先有4个线程争抢锁,线程1,成功了,其他三个失败了,分别依次入等待队列:在这里插入图片描述
线程2、线程3依次入队列:在这里插入图片描述
现在突然发生了点事情,假设线程3用的是带有超时时间的tryLock,超过了等待时间,线程3状态变为取消状态了,这个时候,线程4追加到等待队列中后,发现前一个节点的状态是1取消状态,那么会执行操作把线程3节点从队列中移除掉:
在这里插入图片描述
最后,线程1释放了锁,然后把head节点ws设置为0,并且找到了离head最靠近的一个waitStatus<=0的线程并唤醒,然后参与竞争获取锁:在这里插入图片描述
最终,线程2获取到了锁,然后把自己变为了Head节点,并取代了原来的Head节点:在这里插入图片描述

参考
https://blog.csdn.net/a724888/article/details/60955965

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java技术之AQS详解 的相关文章

随机推荐

  • 大一同学快要期末考试的Python专业课复习 第一章

    第一章 Ptthon 简介 1 1 Python 概述 1 2 搭建 Python 开发环境 1 3 第一个 Python 程序 1 3 1 在IDLE 中编写 Hello World 程序 1 3 2 运行 Python 程序 1 4 P
  • SpringBoot定时任务及分布式锁

    目录 目录 目录 前言 一 定时任务 二 Cron表达式 用短横线 表示时间段 用L表示最后 L是单词Last 最后的 的首字母 Scheduled常用参数的差异 三 分布式锁 分布式锁 项目目中的问题及解决办法 分布式锁的实现 前言 需求
  • 编写测试分析

    编写测试分析的目的 在方向上明确要测什么 怎么测 以及达到什么样质量标准 按照从主到次 从上到下 梳理系统思路 明确测试点 便于对需求的了解和分析 通过编写软件测试分析可间接整理出需求设计的缺陷 提前了解整体测试任务 预测测试风险 测试分析
  • vue项目发布后,webpack源码F12能查看

    正常情况下 webpack 打包的 vue 项目 发布后在浏览器中F12可以直接查看到前端源码 解决这个问题 在 config js 文件中找到 productionSourceMap true 改为 false 如果没有效果 增加 Sou
  • YOLO的训练数据和标注方法是怎样的?如何准备和处理数据集?

    YOLO You Only Look Once 是一种高效的实时目标检测算法 它在训练过程中需要准备适当的数据集和相应的标注 本文将介绍YOLO算法的训练数据和标注方法 以及如何准备和处理数据集 为读者提供一个全面的指南 YOLO的训练数据
  • angular 指令渲染_[Angular 组件库 NG-ZORRO 基础入门] - 待办事项 + 双向绑定

    前言回顾 这几天我们已经完成了 TODO 待办事项 的一些基本功能 涉及多个组件的使用方式 今天我们将 TODO 待办事项 的一些组件独立出来维护 介绍一些组件设计的小方法后 我们将对这个项目里涉及的组件进行一一讲解 有助于大家更加深刻地理
  • 阿里矢量图刷新显示异常

    在vue中使用阿里矢量图遇到了刷新丢失问题 一开始我使用的是在根html中引入的方式 在使用过程中莫名的遇到了刷新丢失问题 于是更改了新的css引入方式 main js中新增引入 import font iconfont css 在vue模
  • 超详细Git下载安装配置使用教程,从入门到上传本地项目到GitHub,一文就够了

    前言 身为一个合格的程序Yuan Git工具我们必然是要掌握的 那么对于小白来讲一定有这几个疑问 Git是什么 怎么使用 怎么配置 Git命令是什么 本文将从这几方面帮助大家了解和使用Git 关于Git Git是一个开源的分布式版本控制系统
  • 前端的图片压缩image-compressor(可在图片上传前实现图片压缩)

    转载自 作者 言墨儿 链接 https www jianshu com p 3ce3e3865ae2 作者 UYOU 链接 https www imooc com article 40038 来源 慕课网 image compressor
  • [网络安全自学篇] 十七.Python攻防之构建Web目录扫描器及ip代理池(四)

    这是作者的系列网络安全自学教程 主要是关于网安工具和实践操作的在线笔记 特分享出来与博友共勉 希望您们喜欢 一起进步 前文分享了Python弱口令攻击 自定义字典生成 调用Python的exrex库实现 并结合Selenium和BurpSu
  • html标签的分类

    HTML标签分类 在HTML页面中 带有 lt gt 符号的元素被称为HTML标签 如上面提到的 都是HTML标签 所谓标签就是放在 lt gt 标签符中表示某个功能的编码命令 也称为HTML标签或 HTML元素 1 双标签 lt 标签名
  • c++之qt学习 基本介绍 界面设计 串口

    这里写目录标题 qt基类介绍 qt不同版本 qt下载 打开qt creater 制作简单qt界面 ui界面 点击forms 双击ui文件 就可以进入ui编辑器 qt信号和槽 给界面增加图片 界面布局 布局不会影响代码 界面切换 更改代码 验
  • ctf.show web 刷题记录

    文章目录 红包题第二弹 web13 web14 方法一 方法二 红包题第六弹 红包题第二弹 打开题目 提示参数cmd 我们随便输入 cmd 1 得到源代码 ctf show 红包题 where is the flag
  • 微信扫物上线,全面揭秘扫一扫背后的识物技术!

    导语 12月23 日 微信扫物 iOS 版本正式上线 从识别特定编码形态的图片 到精准识别自然场景中商品图片 有哪些难点需要去克服 扫物以图片作为媒介 聚合微信内部有价值的生态内容如电商 百科 资讯进行展示 会催生哪些新的落地场景 本文将细
  • C++编程积累——C++实现十进制与二进制之间的互相转换

    欢迎关注原创公众号 计算机视觉联盟 回复 西瓜书手推笔记 可获取我的机器学习纯手推笔记 直达笔记地址 机器学习手推笔记 GitHub地址 目录 十进制与二进制之间的转换 十进制转换二进制 C 实现十进制转换二进制 二进制转换十进制 C 实现
  • 软件授权与加密技术简单原理

    2019 11 05 当前趋势下 互联网公司一般对外提供服务 而非直接出售软件 所以 大家不怎么关心软件授权 加密 但是 一些工业的软件拥有很核心的算法及技术专利 对外发布时 需要保护好程序 一般有如下要求 不能让未被授权的第三方未经授权而
  • ChatGPT 是什么,有什么作用,跟搜索引擎有什么区别?

    一 ChatGPT 是什么 ChatGPT 是一种自然语言生成的聊天机器人模型 由OpenAI开发 它能够根据用户输入的文本内容 自动生成新的文本内容 它的名称来源于它所使用的技术 GPT 3 架构 即生成式语言模型的第 3 代 当用户在人
  • 基数排序(利用了计数排序):时间复杂度为O(n)、有稳定性

    1 原理 对于数组中所有的元素 利用元素每一位的值进行排序 如十进制元素数组 342 254 87 则先对个位排序 再对十位排序 最后对百位排序 由于十进制每一位范围为0 9 因此按位排序的过程调用计数排序 示意图图下 2 伪代码 假设n个
  • 前端实现西瓜,抖音视频上传,视频帧选取封面功能

    使用纯html css js实现西瓜 抖音视频上传 视频帧选取封面功能 只做了功能 部分优化可以自行修改 效果图片预览 效果视频预览 仿西瓜 抖音视频上传封面图功能 完整代码
  • Java技术之AQS详解

    AbstractQueuedSynchronizer 简写为AQS 抽象队列同步器 它是一个用于构建锁和同步器的框架 许多同步器都可以通过AQS很容易并且高效的构造出来 以下都是通过AQS构造出来的 ReentrantLock Reentr