AQS源码解读

文章目录

  • 前言
  • 一、AQS是什么?
  • 二、解读
    • 重点属性
      • state
      • head、tail
    • 同步变量竞争
      • acquire
    • 同步变量释放
  • 总结


前言

AQS是AbstractQueuedSynchronizer的缩写,也是大神Doug Lea的得意之作。今天我们来进行尽量简化的分析和理解性的代码阅读。


一、AQS是什么?

其实从全称翻译来看,我们其实可以判断出AQS的作用,排队的同步器,或者翻译为“使同步器排队”。所以它的主要作用就是使得线程通过排队的方式进行同步。

二、解读

重点属性

state

state变量是一个volatile的变量,同时内部提供了普通的读写操作,加一个通过Unsafe实现的CAS更写state的方法。CAS更新变量的方法其实就是为了保证对于state变量的更新是线程安全的。

protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

head、tail

既然是队列,那必然要有头尾节点,我们的头尾节点的类型是一个内部类-Node

static final class Node {
        /** Marker to indicate a node is waiting in shared mode */
        static final Node SHARED = new Node();
        /** Marker to indicate a node is waiting in exclusive mode */
        static final Node EXCLUSIVE = null;

        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;

        /**
         * Status field, taking on only the values:
         *   SIGNAL:     The successor of this node is (or will soon be)
         *               blocked (via park), so the current node must
         *               unpark its successor when it releases or
         *               cancels. To avoid races, acquire methods must
         *               first indicate they need a signal,
         *               then retry the atomic acquire, and then,
         *               on failure, block.
         *   CANCELLED:  This node is cancelled due to timeout or interrupt.
         *               Nodes never leave this state. In particular,
         *               a thread with cancelled node never again blocks.
         *   CONDITION:  This node is currently on a condition queue.
         *               It will not be used as a sync queue node
         *               until transferred, at which time the status
         *               will be set to 0. (Use of this value here has
         *               nothing to do with the other uses of the
         *               field, but simplifies mechanics.)
         *   PROPAGATE:  A releaseShared should be propagated to other
         *               nodes. This is set (for head node only) in
         *               doReleaseShared to ensure propagation
         *               continues, even if other operations have
         *               since intervened.
         *   0:          None of the above
         *
         * The values are arranged numerically to simplify use.
         * Non-negative values mean that a node doesn't need to
         * signal. So, most code doesn't need to check for particular
         * values, just for sign.
         *
         * The field is initialized to 0 for normal sync nodes, and
         * CONDITION for condition nodes.  It is modified using CAS
         * (or when possible, unconditional volatile writes).
         */
        volatile int waitStatus;

        
        volatile Node prev;

        
        volatile Node next;

        /**
         * The thread that enqueued this node.  Initialized on
         * construction and nulled out after use.
         */
        volatile Thread thread;

        /**
         * Link to next node waiting on condition, or the special
         * value SHARED.  Because condition queues are accessed only
         * when holding in exclusive mode, we just need a simple
         * linked queue to hold nodes while they are waiting on
         * conditions. They are then transferred to the queue to
         * re-acquire. And because conditions can only be exclusive,
         * we save a field by using special value to indicate shared
         * mode.
         */
        Node nextWaiter;
        }

Node类呢,几个常量属性还是挺好理解的,最难理解的莫过于waitStatus了。理解了这个,对于如何排队,以及排队的时候需要进行哪些操作也就清楚了。

CANCELED:被取消,一般情况是由于同步变量竞争超时或者线程被中断导致的
SIGNAL:说明这个Node的后继节点(继承节点)的thread需要被unpark,也就是唤醒;也就是他的后继节点当前正被阻塞或者待阻塞状态中。
0:初始状态
CONDITION和PROPAGATE先不管了:这俩用在读写锁上的,以后再看看填不填坑

同步变量竞争

acquire

这个方法就是同步变量竞争的最主要方法了。
如果我们自己如果实现排队获取某个属性,最朴素的一般都是直接往队列里添加排队,然后按序处理队列进行同步。但是Doug大神的想法比较全面,而且open。当然也有可能是参考了操作系统里面获取锁的逻辑(我猜的,如果有人知道操作系统层面的做法,可以留言讨论)。Doug大神加了一步tryAcquire。也就是说如果尝试获取失败,再进行排队处理。至于尝试获取是怎么尝试的,需要根据自己的需要进行重写(ReentrantLock、ThreadPoolExecutor等),当然ReentrantLock、ThreadPoolExecutor等类也都是大神自己写的。
而且大神代码精简,如下:

/**
     * Acquires in exclusive mode, ignoring interrupts.  Implemented
     * by invoking at least once {@link #tryAcquire},
     * returning on success.  Otherwise the thread is queued, possibly
     * repeatedly blocking and unblocking, invoking {@link
     * #tryAcquire} until success.  This method can be used
     * to implement method {@link Lock#lock}.
     * 独占模式下进行获取,至少会执行一次tryAcquire,如果成功了,就返回,否则就把线程排队
     * 可能会重复不停的阻塞和解阻塞,执行tryAcquire,直到成功。
     *
     * @param arg the acquire argument.  This value is conveyed to
     *        {@link #tryAcquire} but is otherwise uninterpreted and
     *        can represent anything you like.
     */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

不得不说,注释写得很明白,不过可能很多同学不明白,为什么可能线程会重复阻塞和解阻塞,然后执行tryAcquire呢?
其实是因为即使一个thread被解阻塞(unpark)了,他还是要进行tryAcquire的,但是同时可能正在有新的thread不停加入acquire,然后也在执行tryAcquire,所以unpark了的线程不一定能获取成功,所以如果获取失败,还需要继续unpark。
acquire的主流程示意图

addWaiter
这个方法没什么复杂的,唯一需要注意的是如果没有头结点,是新建了一个空的Node对象作为head和tail,而不是把当前node设置为head,当前node只会设置为tail。所有设置防止并发,都是通过CAS实现

1. 如果tail不为null,就通过CAS的方法设置当前node设置为新的tail,然后返回
2. 如果设置尾结点失败,判断下有没有头结点,如果没有,设置一个空的Node为head和tail,并把当前node设置为新的tail,否则直接把当前node设置为尾结点

acquireQueued
此处也就引入了重复、自旋的概念。
由于所有的Node都排队了,且head并没有关联thread,所以从对头往队尾找thread。
下面这段逻辑是在死循环里的,所以会重复执行,直到获取成功或者由于某些意外情况退出死循环。

3. 是否是head的下一个node,如果是就执行tryAcquire,成功了就把自己设置为head,然后把关联的线程置为null,把原来的head设置为孤儿node
4. 如果不是head或tryAcquire失败了,就需要把前置node的waitStatus设置为SIGNAL,然后阻塞自身,暂停执行死循环

大家可能会对shouldParkAfterFailedAcquire方法逻辑感觉奇怪,为啥非要把前置的且非CANCELED的node的waitStatus设置为SIGNAL?最终达到的效果就是只有当前阻塞的node的waitStatus是0,其他前置node的waitStatus都是SIGNAL
其实waitStatus就是为了进行node状态用的,而SIGNAL状态的含义就是【后继node是阻塞的或者即将阻塞的】,也就是说此时是为了进行打标,避免唤醒的时候“惊群”。
!注:哪怕前置node是head,也要把其waitStatus设置为SIGNAL(毕竟默认的是0)
可以发现Doug大神为了防止滥用CPU,消耗性能,如果获取资源失败,并不会立马重试,而是通过LockSupport.park将当前线程进行阻塞,通过waitStatus来标志这个状态。(不像我们写业务代码,可能就让线程进行不停死循环,直到成功了)
而且Doug大神使用waitStatus这种写法还避免了类似“惊群”的现象,不浪费CPU性能做到了极致

同步变量释放

接下来看release,不过细心的同学一定知道了,release有个最重要的功能就是对一些被阻塞的node进行唤醒,当然就是唤醒SIGNAL的node的下一个非CANCELED的node,而不是所有node,避免“惊群”。

/**
     * Releases in exclusive mode.  Implemented by unblocking one or
     * more threads if {@link #tryRelease} returns true.
     * This method can be used to implement method {@link Lock#unlock}.
     *
     * @param arg the release argument.  This value is conveyed to
     *        {@link #tryRelease} but is otherwise uninterpreted and
     *        can represent anything you like.
     * @return the value returned from {@link #tryRelease}
     */
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

和tryAcquire一样,tryRelease是真正执行释放资源的操作,需要实现。
主逻辑就是如果释放成功了,需要唤醒后继第一个非CANCELED的node。
unparkSuccessor

1. 将node(此时的node可能是head也可能是被cancel的node)的waitStatus设置为0,因为需要唤醒下一个node成为新的head了
2. 找到后继第一个非CANCELED的node,进行unpark,使得其继续执行acquireQueued,来竞争获取资源

这个方法里最有意思的一个逻辑是如果后继node是一个CANCELED了的node,那么没有继续这个CANCELED的node的next,而是基于tail从后往前找,找到离head最近的SIGNAL状态的node。
原因是因为当某个node被cancelAcquire的时候,会把该节点从队列里移除,但是因为方法逻辑是先设置状态,后移除,移除之后,node的next就会被设置为null。
所以此处基于tail从后往前找的原因就是防止并发导致读取到node的状态是CANCELED,然后后面基于next往后遍历读取到null,而反过来则不会出问题,因为没有把后继节点的pre设置为null。
CANCEL的队列示意图

可以发现,节点被cancelAcquire的时候,只会把prev和当前node的next设置为null,而所有节点的prev指向都没变。所以从后往前找非CANCELED的node一定没问题。

如果某个node被执行了cancelAcquire,也会unpark后继节点。
所以release和cancelAcquire都会unpark后继节点。

总结

竞争资源总体分三步

1. 尝试获取资源,成功就返回
2. 失败了的线程封装成Node,加入到队列
3. 加入到队列的Node,不停经历(阻塞-非阻塞-tryAcquire),直到获取资源成功

释放资源分两步

4. 尝试释放资源,失败了就返回
5. 释放成功了,需要把头结点的waitStatus置空,唤醒下一个非CANCELED的Node进行资源获取操作

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

旧版本docker未及时更新,导致更新/etc/docker/daemon.json配置文件出现docker重启失败

一、背景 安装完docker和containerd之后,尝试重启docker的时候,报错如下: systemctl restart dockerJob for docker.service failed because the control process exited with error code. See “systemctl status docker.service” and “…

android设置竖屏仍然跟随屏幕旋转怎么办

如题所问,我最近遇到一个bug,就是设置了摇感,然后有用户反馈说设置了手机下拉的系统设置-屏幕旋转-关闭。然后屏幕还是会旋转的问题。 首先,我们先从如何设置横竖屏了解下好了 设置横屏和竖屏的方法: 方法一&#x…

算法与数据结构(七)--堆

一.堆 1.堆的定义 堆是计算机科学中一类特殊的数据结构的通常,堆通常可以被看做是一颗完全二叉树的数组对象。 堆的特性 1.它是完全二叉树,除了树的最后一层结点不需要是满的,其他的每一层从左到右都是满的,如果最后一层结点不…

PyTorch学习笔记(十三)——现有网络模型的使用及修改

以分类模型的VGG为例 vgg16_false torchvision.models.vgg16(weightsFalse) vgg16_true torchvision.models.vgg16(weightsTrue) 设置为 False 的情况,相当于网络模型中的参数都是初始化的、默认的设置为 True 时,网络模型中的参数在数据集上是训练好…

Docker中为RabbitMQ安装rabbitmq_delayed_message_exchange延迟队列插件

1、前言 rabbitmq_delayed_message_exchange是一款向RabbitMQ添加延迟消息传递(或计划消息传递)的插件。 插件下载地址:https://www.rabbitmq.com/community-plugins.html 1、下载插件 首先需要确定我们当前使用的RabbitMQ的版本&#xff0c…

九耶丨阁瑞钛伦特-Spring boot与Spring cloud 之间的关系

Spring Boot和Spring Cloud是两个相互关联的项目,它们可以一起使用来构建微服务架构。 Spring Boot是一个用于简化Spring应用程序开发的框架,它提供了自动配置、快速开发的特性,使得开发人员可以更加轻松地创建独立的、生产级别的Spring应用程…

Unity UI内存泄漏优化

项目一运行,占用的内存越来越多,不会释放,导致GC越来越频繁,越来越慢,这些都是为什么呢,今天从UI方面谈起。 首先让我们来聊聊什么是内存泄漏呢? 一般来讲内存泄漏就是指我们的应用向内存申请…

Lnton羚通关于PyTorch的保存和加载模型基础知识

SAVE AND LOAD THE MODEL (保存和加载模型) PyTorch 模型存储学习到的参数在内部状态字典中,称为 state_dict, 他们的持久化通过 torch.save 方法。 model models.shufflenet_v2_x0_5(pretrainedTrue) torch.save(model, "../../data/ShuffleNetV2_X0.5.pth…

【C++】AVL树(平衡二叉树)

目录 一、AVL树的定义二、AVL树的作用三、AVL树的插入操作插入——平衡因子的更新插入——左单旋插入——右单旋插入——左右双旋插入——右左双旋 四、ALVL树的验证五、AVL树的性能 一、AVL树的定义 AVL树,全称 平衡二叉搜索(排序)树。 二…

使用SpringBoot + Thymeleaf 完成简单的用户登录

😀前言 本篇博文是关于Thymeleaf 的综合案例, 使用SpringBoot Thymeleaf 完成简单的用户登录-列表功能,希望你能够喜欢😊 🏠个人主页:晨犀主页 🧑个人简介:大家好,我是晨…

​Kubernetes的演变:从etcd到分布式SQL的过渡

DevRel领域专家Denis Magda表示,他偶然发现了一篇解释如何用PostgreSQL无缝替换etcd的文章。该文章指出,Kine项目作为外部etcd端点,可以将Kubernetes etcd请求转换为底层关系数据库的SQL查询。 受到这种方法的启发,Magda决定进一步…

Cat(5):API介绍—Event

Event 用来记录一件事发生的次数,比如记录系统异常,它和transaction相比缺少了时间的统计,开销比transaction要小。 Cat.logEvent 记录一个事件。 Cat.logEvent("URL.Server", "serverIp", Event.SUCCESS, "ip${…

如何进行远程debug?

文章目录 前言一、使用步骤1.首先通过nohup在启动jar包的我们可以添加参数:2.具体参数的含义如下:3. 查询监听的端口: 前言 在工作中,排查问题我们经常需要进行debug,而远程debug能够方便的帮助我们排查线上的问题。 …

【力扣】496. 下一个更大元素 I <单调栈、模拟>

【力扣】496. 下一个更大元素 I nums1 中数字 x 的 下一个更大元素 是指 x 在 nums2 中对应位置 右侧 的 第一个 比 x 大的元素。给你两个没有重复元素的数组 nums1 和 nums2 &#xff0c;下标从 0 开始计数&#xff0c;其中nums1 是 nums2 的子集。   对于每个 0 < i <…

【AIGC】 国内版聊天GPT

国内版聊天GPT 引言一、国内平台二、简单体验2.1 提问2.2 角色扮演2.3 总结画图 引言 ChatGPT是OpenAI发开的聊天程序&#xff0c;功能强大&#xff0c;可快速获取信息&#xff0c;节省用户时间和精力&#xff0c;提供个性化的服务。目前国产ChatGPT&#xff0c;比如文心一言&a…

Kubernetes二进制部署方案

目录 一、环境准备 2.1、主机配置 2.2、安装 Docker 2.3、生成通信加密证书 2.3.1、生成 CA 证书&#xff08;所有主机操作&#xff09; 2.3.2、生成 Server 证书&#xff08;所有主机&#xff09; 2.3.3、生成 admin 证书(所有主机) 2.3.4、生成 proxy 证书 三、部署 …

JMeter接口自动化测试实例—JMeter引用javaScript

Jmeter提供了JSR223 PreProcessor前置处理器&#xff0c;通过该工具融合了Java 8 Nashorn 脚本引擎&#xff0c;可以执行js脚本以便对脚本进行前置处理。其中比较典型的应用就是通过执行js脚本对前端数据进行rsa加密&#xff0c;如登录密码加密。但在这里我就简单的应用javaScr…

java练习6. 求完数

题目: 请编程求出1000 以内的所有完数。 完数:一个数如果恰好等于它的所有真因子&#xff08;即除了自身外的所有因数&#xff09;之和&#xff0c;这个数就称为"完数"。 public static void main(String[] args) {for (int i 2; i < 1000; i) {int sum0;for (in…

ARM M33架构入门

概述 Arm Cortex-M33核心处理器专为需要高效安全或数字信号控制的物联网和嵌入式应用而设计。该处理器具有许多可选功能&#xff0c;包括数字信号处理扩展 (DSP)、用于硬件强制隔离的TrustZone 安全性、内存保护单元 (MPU)和浮点单元 (FPU)。 Cortex-M33 的性能比 Cortex-M…

【笔试题心得】关于正则的一些整理

本文部分内容摘抄整理自 正则表达式 – 教程 | 菜鸟教程 在笔试的过程中&#xff0c;也常常会对正则表达式进行考察&#xff0c;这里对正则表达式的常见用法&#xff0c;做一个学习和总结。 正则表达式的模式可以包括以下内容&#xff1a; 字面值字符&#xff1a;例如字母、数…