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 thisreturn 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变为head时,才会把自身的prev设置为null

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

总结

竞争资源总体分三步

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

释放资源分两步

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

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

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

相关文章

回归预测 | MATLAB实现WOA-SVM鲸鱼算法优化支持向量机多输入单输出回归预测(多指标,多图)

回归预测 | MATLAB实现WOA-SVM鲸鱼算法优化支持向量机多输入单输出回归预测(多指标,多图) 目录 回归预测 | MATLAB实现WOA-SVM鲸鱼算法优化支持向量机多输入单输出回归预测(多指标,多图)效果一览基本介绍程…

PostgreSQL中的密码验证方法

假设您想在客户端/服务器协议中实现密码身份验证方法。 您将如何做到这一点以及可能出现的问题是什么? 以下是 PostgreSQL 中如何完成此操作的故事。 password 一开始,PostgreSQL 只有 pg_hba.conf 中现在称为“password”的方法。 这是你能想象到的最…

CloudQuery:更好地管理你的 OceanBase 数据库

前言:作为 OceanBase 的生态合作伙伴,CloudQuery(简称“CQ”) 最新发布的社区版 2.2.0 新增了 OceanBase 数据库,为企业使用 OceanBase 数据库提供全面的支持。包括连接与认证、查询与分析、数据安全与权限管理&#x…

【仿写tomcat】三、通过socket读取http请求信息

仿写tomcat 建立Socket连接获取连接信息查看HTTP信息 建立Socket连接 这里我们也是创建一个专门管理socket的类 package com.tomcatServer.socket;import java.io.*; import java.net.ServerSocket;/*** 套接字存储** author ez4sterben* date 2023/08/15*/ public class Soc…

【校招VIP】java语言考点之List和扩容

考点介绍: List是最基础的考点,但是很多同学拿不到满分。本专题从两种实现子类的比较,到比较复杂的数组扩容进行分析。 『java语言考点之List和扩容』相关题目及解析内容可点击文章末尾链接查看!一、考点题目 1、以下关于集合类…

操作系统练习:在Linux上创建进程,及查看进程状态

说明 进程在执行过程中可以创建多个新的进程。创建进程称为“父进程”,新的进程称为“子进程”。每个新的进程可以再创建其他进程,从而形成进程树。 每个进程都有一个唯一的进程标识符(process identifier,pid)。在L…

通过远程访问解决家人的电脑难题

家里老人使用电脑、手机等设备遇到问题,子女在其他城市没法现场解决,远程沟通很困难。有没有一种方法可以随时远程帮助家人解决电脑、手机相关问题? 通过远程桌面软件,你可以与父母一起实时解决问题。这样他们就不会一遍遍给你打…

Linux系统的目录结构

file system hierarchy standard文件系统层级标准,定义了在类Unix系统中的目录结构和目录内容。 即让用户了解到已安装软件通常放置于哪个目录下。 Linux目录结构的特点 使用树形目录结构来组织和管理文件。 整个系统只有一个根目录(树根)&a…

Java虚拟机(JVM):垃圾收集算法

目录 一、分代收集理论 二、标记-清除算法 三、标记-复制算法 四、标记-整理算法 一、分代收集理论 分代收集理论建立在两个分代假说之上: 1、弱分代假说:绝大多数对象都是朝生夕灭的。 2、强分代假说:熬过越多次垃圾收集过程的对象就…

rust入门系列之Rust介绍及开发环境搭建

Rust教程 Rust基本介绍 网站: https://www.rust-lang.org/ rust是什么 开发rust语言的初衷是: 在软件发展速度跟不上硬件发展速度,无法在语言层面充分的利用硬件多核cpu不断提升的性能和 在系统界别软件开发上,C出生比较早,内…

排序(七种排序)

1.插入排序 2.希尔排序 3.选择排序 4.堆排序 5.冒泡排序 6.快速排序 7.归并排序 1.插入排序 1.1思路 把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为 止,得到一个新的有序序列 1.2实现 //插入排…

部署LVS-DR群集

目录 DR模式 LVS负载均衡群集 数据包流向分析 DR 模式的特点 LVS-DR中的ARP的问题 问题1 问题2 DR模式 LVS负载均衡群集部署 部署192.168.142.21(DR服务器) 部署192.168.142.22、192.168.142.23(Web 服务器1、Web 服务器2&#xff0…

(成功踩坑)electron-builder打包过程中报错

目录 注意:文中的解决方法2,一定全部看完,再进行操作,有坑 背景 报错1: 报错2: 1.原因:网络连接失败 2.解决方法1: 3.解决方法2: 3.1查看缺少什么资源文件 3.2去淘…

云曦暑期学习第六周——kali

1.熟悉网络配置 一般来说虚拟机有三种网络模式: NAT (网络地址转换模式)Bridged (桥接模式)Host-Only(主机模式) nat模式: 虚拟系统会通过宿主机的网络来访问外网。而这里的宿主机相当于有两个网卡,一个是真实网卡…

SpringCloud Ribbon中的7种负载均衡策略

SpringCloud Ribbon中的7种负载均衡策略 Ribbon 介绍负载均衡设置7种负载均衡策略1.轮询策略2.权重策略3.随机策略4.最小连接数策略5.重试策略6.可用性敏感策略7.区域敏感策略 总结 负载均衡通器常有两种实现手段,一种是服务端负载均衡器,另一种是客户端…

MongoDB 安装 linux

本文介绍一下MongoDB的安装教程。 系统环境:CentOS7.4 可以用 cat /etc/redhat-release 查看本机的系统版本号 一、MongoDB版本选择 当前最新的版本为7.0,但是由于7.0版本安装需要升级glibc2.25以上,所以这里我暂时不安装该版本。我们选择的是6.0.9版本…

互斥锁的概念,与部分接口

何为互斥 一种对共享数据的保护,防止多线程同时访问共享资源的时,数据混乱的问题。在互斥期间,保证执行流由并行改为串行。任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起…

时序预测 | MATLAB实现ELM极限学习机时间序列预测(多指标、相关图)

时序预测 | MATLAB实现ELM极限学习机时间序列预测(多指标、相关图) 目录 时序预测 | MATLAB实现ELM极限学习机时间序列预测(多指标、相关图)效果一览基本介绍程序设计学习总结参考资料效果一览 基本介绍 时序预测 | MATLAB实现ELM极

[HZNUCTF 2023 preliminary] 2023杭师大校赛(初赛) web方向题解wp 全

ezflask 先看题目,应该是模板注入(SSTI),输入{{7*‘7’}}直接报错误。 发现模板是反序输出的,怪不得不能直接输入{{}}。 输入}}‘7’*7{{返回777777,是jinja2 //直接手打,无所谓我是怨种 ?nam…

【数理知识】向量的坐标基表示法,Matlab 代码验证

序号内容1【数理知识】向量的坐标基表示法,Matlab 代码验证2【数理知识】向量与基的内积,Matlab 代码验证 文章目录 1. 向量的坐标基表示2. 二维平面向量举例3. Matlab 代码验证Ref 1. 向量的坐标基表示 假设空间中存在一个向量 a ⃗ \vec{a} a &#…