AQS源码解析
关于 AQS,网上已经有无数的文章阐述 AQS 的使用及其源码,所以多这么一篇文章也没啥所谓,还能总结一下研究过的源码。源码解析和某某的使用,大概是互联网上 Java 文章中写得最多的主题了。
AQS
AQS 是 AbstractQueuedSynchronizer 的缩写,中文翻译过来就是抽象队列同步器。ReentrantLock
、ReentrantReadWriteLock
、Semaphore
、CountDownLatch
都是基于 AQS。AQS 的核心思想是,当线程请求获取资源时,如果资源空闲,则会将当前线程设置为资源的独占线程,成功获得锁;否则将获取锁失败的线程加入到排队队列中(CLH),并提供线程阻塞和线程唤醒机制。CLH 是一个虚拟的双向队列。
首先看一下 AQS 的关键属性。
// java.util.concurrent.locks.AbstractQueuedSynchronizer
private transient volatile Node head; //队列的头节点
private transient volatile Node tail; //队列的尾节点
private volatile int state; //同步状态
state
用于实现锁的可重入性。
- 0 为表示没有线程持有该锁
- 当存在线程获取锁成功,则 state 变为 1。如果是同一线程重复获得,则 state++
- 如果存在线程释放锁,则 state–
上面的三个属性都存在对应的以 CAS 方式进行修改的方法,state
对应的是 compareAndSetState()
方法, head
对应的是 compareAndSetHead()
方法,tail
对应的是 compareAndSetTail()
。以 CAS 的方式修改值能避免锁的竞争。
因为请求获取锁的线程会以 Node 节点的方式在 CLH 队列中排队,在分析 AQS 机制时也会大量涉及到 Node 节点,所以很有必要对 Node 节点进行分析。
CLH 队列中 Node 节点。
// java.util.concurrent.locks.AbstractQueuedSynchronizer.Node
volatile int waitStatus; //当前节点在队列中的状态
volatile Node prev; //前驱节点
volatile Node next; //后继节点
volatile Thread thread; //在该节点中排队的线程
Node nextWaiter; //下一个处于condition或共享状态的节点//等待锁的两种状态
static final Node SHARED = new Node(); //共享
static final Node EXCLUSIVE = null; //独占//waitStatus的几个值
static final int CANCELLED = 1; //已取消
static final int SIGNAL = -1; //后继节点的线程需要唤醒
static final int CONDITION = -2; //节点处于等待队列中
static final int PROPAGATE = -3; //线程处在SHARED情况下使用该字段
ReentrantLock
说是研究 AQS 源码,但 AQS 毕竟是一个抽象类,只实现了部分方法,另外一些方法会在子类中实现。所以我们也同样会涉及到 ReentrantLock 源码的研究。
ReentrantLock 的通常使用方式是:
class X { private final ReentrantLock lock = new ReentrantLock(); public void m() { lock.lock(); try { // ... method body } finally { lock.unlock() } }
}
ReentrantLock 存在两种锁:公平锁(FairSync)和非公平锁(NonfairSync),默认为非公平锁。使用公平锁时,线程会直接进入队列中排队,只有队列中第一个线程才能获取锁;使用非公平锁时,线程会先尝试获取锁,成功则占用锁,失败则在队列排队。对于 AQS 来说,公平锁和非公平锁的绝大部分方法都是共用的。
ReentrantLock 的主要方法有两个,一是使用 lock()
方法加锁,二是使用 unlock()
方法解锁。
加锁
非公平锁
我们首先来分析非公平锁的加锁过程。
// java.util.concurrent.locks.ReentrantLock.NonfairSync
final void lock() { if (compareAndSetState(0, 1)) //通过cas方式设置同步状态setExclusiveOwnerThread(Thread.currentThread()); //成功,设置为独占线程else acquire(1); //失败,获取锁
}
在 lock()
方法中,会先尝试通过 CAS 的方式去获取锁,成功则设置为独占线程,否则执行 acquire()
方法。
// java.util.concurrent.locks.AbstractQueuedSynchronizer
public final void acquire(int arg) { if (!tryAcquire(arg) && //尝试获取锁acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 失败,则添加到等待队列selfInterrupt();
}
在 acquire()
方法中,会再次尝试去获取锁,如果失败则加入到排队队列。
// java.util.concurrent.locks.ReentrantLock.NonfairSync#tryAcquire
protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires);
}// java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { //如果当前没有线程持有锁if (compareAndSetState(0, acquires)) { //state++setExclusiveOwnerThread(current); //将当前线程设置为独占线程return true; } } else if (current == getExclusiveOwnerThread()) { //如果已有线程持有锁,且当前线程为独占线程int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); //state++,实现锁的可重入性return true; //获得锁} return false; //如果已有线程持有锁,且当前线程不为独占线程,则获取锁失败
}
在 nonfairTryAcquire()
方法中,如果资源空闲,则再次尝试通过 CAS 的方式去获取锁。如果当前资源已被当前线程占用,则将 state++
,以实现锁的可重入性。
// java.util.concurrent.locks.AbstractQueuedSynchronizer#addWaiter
// 设置队列尾节点
private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); //新建排队节点 Node pred = tail; //pred指向尾节点if (pred != null) { //如果Pred指针是Null(说明等待队列中没有元素),或者当前Pred指针和Tail指向的位置不同(说明被别的线程已经修改)node.prev = pred; //将新建节点的prev指向predif (compareAndSetTail(pred, node)) { // 设置新建节点为尾节点pred.next = node; return node; } } enq(node); return node;
}//java.util.concurrent.locks.AbstractQueuedSynchronizer#enq
private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { //如果尾节点为空,则说明队列还未初始化,if (compareAndSetHead(new Node())) //初始化一个头节点tail = head; } else { //如果已经初始化,则设置为尾节点node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } }
}
如果尝试多次获取锁都失败,则在 addWaiter()
方法中会将线程放到节点中,并设置为排队队列的尾节点。
//java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireQueued
final boolean acquireQueued(final Node node, int arg) { //是否成功获取到资源boolean failed = true; try { //循环等待过程中是否中断过boolean interrupted = false; for (;;) { final Node p = node.predecessor(); //获取当前节点的前驱节点if (p == head && tryAcquire(arg)) { //如果前驱节点为头节点,则尝试获取锁setHead(node); //获取锁成功,设置当前节点为头节点p.next = null; // help GC failed = false; return interrupted; } //来到这里,说明前驱节点不是头节点或者获取锁失败。判断当前节点是否要被阻塞if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; //线程中断成功} } finally { if (failed) cancelAcquire(node); }
}
加入到排队队列中后,会在 acquireQueued()
方法中循环等待资源的获取,并判断线程是否需要被阻塞,直到线程获取成功或者抛出异常。
//java.util.concurrent.locks.AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire
//靠前驱节点判断当前线程是否应该被阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { //获取前驱节点的状态int ws = pred.waitStatus; if (ws == Node.SIGNAL) //如果前驱节点处于唤醒状态 return true; if (ws > 0) { //前驱节点处于取消状态 do { //向前查找取消的节点,并将其从队列中删除node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { compareAndSetWaitStatus(pred, ws, Node.SIGNAL); //设置前驱节点为唤醒状态} return false;
}
//java.util.concurrent.locks.AbstractQueuedSynchronizer#parkAndCheckInterrupt
//挂起当前线程,阻塞调用栈,返回当前线程的中断状态
private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted();
}
整一个非公平锁的加锁流程可以用如下流程图表示:
公平锁
公平锁和非公平锁的流程中只有 lock()
方法和 tryAcquire()
方法存在差别。
//java.util.concurrent.locks.ReentrantLock.FairSync#lock
final void lock() { acquire(1); //直接获取锁
}
公平锁中会直接调用 acquire()
方法。
// java.util.concurrent.locks.ReentrantLock.FairSync#tryAcquire
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false;
}
相对于 nonfair
的 nonfairTryAcquire
方法,在没有线程持有锁时,增加了 hasQueuedPredecessors()
方法的判断,该方法用于判断队列中是否存在线程比当前线程等待时间更长。
// java.util.concurrent.locks.AbstractQueuedSynchronizer#hasQueuedPredecessors
public final boolean hasQueuedPredecessors() { Node t = tail; Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
分析一下这段代码:
- 如果
h != t
为 true,说明队列正在初始化,或者已经初始化完成。- 在
h != t
前提下,如果(s = h.next) == null
为 true,说明队列正在初始化,因为在队列初始化过程中,是有可能存在 head 已经被初始化(不再等于 tail 了),但 head.next 还没有被设值为 node,这种情况下,因为队列中已经存在 Node,当前线程需要加到等待队列中,故返回 true。初始化过程在AbstractQueuedSynchronizer#enq()
。 - 但如果
(s = h.next) == null
为 false,说明队列中已经存在 Node,则判断该 Node 的线程是否与当前线程相同。如果s.thread != Thread.currentThread()
为 true,说明不相同,需要进入等待队列。如果相同,说明当前线程可以获取锁。
- 在
- 如果
h != t
为 false,说明队列为空,返回 false,说明可以去获得锁。
另外,s = h.next
这段代码获取的是 head 的下一个节点,因为 head 是虚节点,不存储数据,真正的数据存储在 head.next
。
解锁
相对于加锁过程,解锁过程比较简单,且公平锁和非公平锁共用同一个 lock()
方法。
// java.util.concurrent.locks.ReentrantLock#unlock
public void unlock() { sync.release(1);
}//java.util.concurrent.locks.AbstractQueuedSynchronizer#release
public final boolean release(int arg) { if (tryRelease(arg)) { //如果锁没有被任何线程持有Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); //解除后继节点的线程挂起return true; } return false;
}
关于代码 h != null && h.waitStatus != 0
,分为以下几种情况:
h==null
,说明头节点还未初始化。h!=null && h.waitStatus==0
,说明后继节点还在运行中。因为如果节点已被取消,waitStatus 会被设置为 1;如果后继节点需要唤醒,waitStatus 会被设置为 -1;如果节点正在排队,waitStatus 则会设置为 -2。如果 waitStatus 为 0,说明已经处于运行中。h != null && h.waitStatus != 0
则会去唤醒后继节点。
在 release()
方法中,会先尝试去解锁,如果解锁成功且后继节点需要唤醒,则将后继节点取消挂起。
//java.util.concurrent.locks.ReentrantLock.Sync#tryRelease
//更新state状态,如果重入次数为0,则将锁的独占线程设置为null
protected final boolean tryRelease(int releases) { //减少可重入次数int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) //如果当前线程不是该锁的独占线程,则抛出异常throw new IllegalMonitorStateException(); boolean free = false; //该锁是否已被释放if (c == 0) { //如果可重入次数已为0free = true; setExclusiveOwnerThread(null); //将锁的独占线程设置为null,表明不再有线程持有该锁} setState(c); //修改锁的状态return free;
}
在 tryRelease()
方法中,会去减少锁的可重入次数,当可重入次数为 0 时,清空锁的独占线程。
// java.util.concurrent.locks.AbstractQueuedSynchronizer#unparkSuccessor
private void unparkSuccessor(Node node) { int ws = node.waitStatus; //获取头节点的waitStatusif (ws < 0) //如果节点的waitStatus为负数,说明后继节点需要被唤醒或者正在排队compareAndSetWaitStatus(node, ws, 0); //清除节点当前的waitStatus,设置为0 Node s = node.next; //获取头节点的后继节点if (s == null || s.waitStatus > 0) { //如果节点为null,或者已被取消s = null; for (Node t = tail; t != null && t != node; t = t.prev) //从尾节点开始向前遍历if (t.waitStatus <= 0) //找到队列中第一个不被取消的节点s = t; } if (s != null) //如果找到了,则将该节点取消挂起LockSupport.unpark(s.thread);
}
在 unparkSuccessor()
方法中,会从尾节点开始从后往前遍历,找到队列中第一个没有被取消的节点,将该节点取消挂起。
整个解锁流程可以用如下流程图表示。
相关文章:

AQS源码解析
关于 AQS,网上已经有无数的文章阐述 AQS 的使用及其源码,所以多这么一篇文章也没啥所谓,还能总结一下研究过的源码。源码解析和某某的使用,大概是互联网上 Java 文章中写得最多的主题了。 AQS AQS 是 AbstractQueuedSynchronize…...

关于在VS2017中编译Qt项目遇到的问题
关于在VS2017中编译Qt项目遇到的问题 【QT】VS打开QT项目运行不成功 error MSB6006 “cmd.exe”已退出,代码为 2。如何在VS2017里部署的Qt Designer上编辑槽函数 【QT】VS打开QT项目运行不成功 error MSB6006 “cmd.exe”已退出,代码为 2。 链接 如何在VS2017里部署的Qt Design…...

Python web实战 | 使用 Flask 实现 Web Socket 聊天室
概要 今天我们学习如何使用 Python 实现 Web Socket,并实现一个实时聊天室的功能。本文的技术栈包括 Python、Flask、Socket.IO 和 HTML/CSS/JavaScript。 什么是 Web Socket? Web Socket 是一种在单个 TCP 连接上进行全双工通信的协议。它是 HTML5 中的…...

Android10 Recovery系列(一)隐藏recovery菜单项
一 、背景 起因是遇到了一个隐藏删除recovery菜单项的需求。在寻找解决问题的时候,我经历了找到源码位置,调试修改,生效,思考是否可拓展,优化修改,符合要求的整个过程,下面简单分享一下。如果不想立即实现效果或者只想看解决方案,可以直接看总结那一个部分 二 、准备…...

选好NAS网络储存解决方案,是安全储存的关键
随着网络信息的发展,NAS也越来越受到企业的关注,NAS网络存储除了提供简单的存储服务外,还可以提供更好的数据安全性、更方便的文件共享方式。但市面上的产品种类繁多,我们该如何选择合适的产品,通过企业云盘࿰…...

AnimateDiff论文解读-基于Stable Diffusion文生图模型生成动画
文章目录 1. 摘要2. 引言3. 算法3.1 Preliminaries3.2. Personalized Animation3.3 Motion Modeling Module 4. 实验5.限制6. 结论 论文: 《AnimateDiff: Animate Your Personalized Text-to-Image Diffusion Models without Specific Tuning》 github: https://g…...

centos7安装tomcat
安装tomcat 必须依赖 JDK 环境,一定要提前装好JDK保证可以使用 一、下载安装包 到官网下载 上传到linux 服务器 二、安装tomcat 创建tomcat 文件夹 mkdir -p /usr/local/tomcat设置文件夹权限 chmod 757 tomcat将安装包上传至 新建文件夹 解压安装包 tar zx…...

【C#教程】零基础从入门到精通
今天给大家分享一套零基础从入门到精通:.NetCore/C#视频教程;这是2022年最新整理的、590G的开发教程资料。课程涵盖了.Net各方面的知识,跟着这个教程学习,就足够了。 课程分类 1、C#从基础到精通教程; 2、Winform从…...

opencv rtsp 硬件解码
讨论使用opencv的reader 硬件解码的方案有太多种,如果使用ffmpeg硬件解码是最方便的,不方便的是把解码过后的GPU 拉到 CPU 上,再使用opencv的Mat 从cpu 上上载到gpu上,是不是多了两个过程,应该是直接从GPU mat 直接去…...

机器学习-Gradient Descent
机器学习(Gradient Descent) videopptblog 梯度下降(Gradient Descent) optimization problem: 损失函数最小化 假设本模型有两个参数𝜃1和𝜃2,随机取得初始值 求解偏微分,梯度下降对参数进行更新 Visualize: 确定梯度方向&…...

MySql003——SQL(结构化查询语言)基础知识
一、数据库的相关概念 DB:数据库(Database) 即存储数据的“仓库”,其本质是一个文件系统。它保存了一系列有组织的数据。DBMS:数据库管理系统(Database Management System) 是一种操纵和管理数…...

springCloud Eureka注册中心配置详解
1、创建一个springBoot项目 2、在springBoot项目中添加SpringCloud依赖 <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>2021.0.3</version><type>…...

gti 远程操作
目录 一. 分布式版本控制管理系统 1. 理解分布式版本控制管理系统 二. 创建远程仓库 编辑 编辑 三. 克隆远程仓库_HTTP 四. 克隆远程仓库_SSH 配置公钥 添加公钥 五. git 向远程仓库推送 六. 拉取远程仓库 七. 忽略特殊文件 八. 配置别名 一. 分布式版本控制管理…...

Ftrace
一、概述 Ftrace有剖析器和跟踪器。剖析器提供统计摘要,如激素胡和直方图;而跟踪器提供每一个事件的细节。 Ftrace剖析器列表: 剖析器描述function内核函数统计分析kprobe profiler启用的kprobe计数器uprobe profiler启用的uprobe计数器hi…...

Tomcat修改端口号
网上的教程都比较老,今天用tomcat9.0记录一下 conf文件夹下server.xml文件 刚开始改了打红叉的地方,发现没用,改了上面那行...

vue2企业级项目(一)
vue2企业级项目(一) 创建项目,并创建项目编译规范 1、node 版本 由于是vue2项目,所以 node 版本比较低。使用 12.18.3 左右即可 2、安装vue 安装指定版本的vue2 npm i -g vue2.7.10 npm i -g vue/cli4.4.63、编辑器规范 vsc…...

【前端知识】React 基础巩固(三十八)——log、thunk、applyMiddleware中间件的核心代码
React 基础巩固(三十八)——log、thunk、applyMiddleware中间件的核心代码 一、打印日志-中间件核心代码 利用Monkey Patching,修改原有的程序逻辑,在调用dispatch的过程中,通过dispatchAndLog实现日志打印功能 // 打印日志-中间件核心代码…...

hive删除数据进行恢复
在实际开发或生产中,hive表如果被误删,如被truncate或是分区表的分区被误删了,只要在回收站的清空周期内,是可以恢复数据的,步骤如下: (1) 先找到被删除数据的存放目录,…...

二、前端高德地图、渲染标记(Marker)引入自定义icon,手动设置zoom
要实现这个效果,我们先看一下目前的页面展示: 左边有一个图例,我们可以方法缩小地图,右边是动态的marker标记,到时候肯定时候是后端将对应的颜色标识、文字展示、坐标点给咱们返回、我们肯定可以拿到一个list…...

UDF和UDAF、UDTF的区别
UDF UDF(User-defined functions)用户自定义函数,简单说就是输入一行输出一行的自定义算子。 是大多数 SQL 环境的关键特性,用于扩展系统的内置功能。(一对一) UDAF UDAF(User Defined Aggregat…...

小研究 - 浅析 JVM 中 GC 回收算法与垃圾收集器
本文主要介绍了JVM虚拟机中非常重要的两个部分,GC 回收算法和垃圾收集器。从可回收对象的标记开始,详细介绍 了四个主流的GC算法,详细总结了各自的算法思路及优缺点, 提出了何种情况下应该通常选用哪种算法。 目录 1 标记可回收对…...

Flowable-服务-骆驼任务
目录 定义图形标记XML内容Flowable与Camel集成使用示例设计Came路由代码 定义 Camel 任务不是 BPMN 2.0 规范定义的官方任务,在 Flowable 中,Camel 任务是作为一种特殊的服务 任务来实现的。主要做路由工作的。 图形标记 由于 Camel 任务不是 BPMN 2.…...

用html+javascript打造公文一键排版系统9:主送机关排版
一、主送机关的规定 公文一般在标题和正文之间还有主送机关,相关规定为: 主送机关 编排于标题下空一行位置,居左顶格,回行时仍顶格,最后一个机关名称后标全角冒号。如主送机关名称过多导致公文首页不能显示正文时&…...

SpringBoot 集成 EasyExcel 3.x 优雅实现 Excel 导入导出
介绍 EasyExcel 是一个基于 Java 的、快速、简洁、解决大文件内存溢出的 Excel 处理工具。它能让你在不用考虑性能、内存的等因素的情况下,快速完成 Excel 的读、写等功能。 EasyExcel文档地址: https://easyexcel.opensource.alibaba.com/ 快速开始 …...

RT1052 的四定时器
文章目录 1 Quad Timer,简称:QTMR2 单个通道的框图3 QTMR配置3.1 QTMR1 时钟使能。3.2 初始化 QTMR1。3.2.1 QTMR_Init 3.3 设置 QTMR1 通道 0 的定时周期。3.3.1QTMR_SetTimerPeriod 3.4 使能 QTMR1 通道 0 的比较中断。3.4.1 QTMR_EnableInterrupts 3.…...

ViT-vision transformer
ViT-vision transformer 介绍 Transformer最早是在NLP领域提出的,受此启发,Google将其用于图像,并对分类流程作尽量少的修改。 起源:从机器翻译的角度来看,一个句子想要翻译好,必须考虑上下文的信息&…...

Election of the King 2023牛客暑期多校训练营4-F
登录—专业IT笔试面试备考平台_牛客网 题目大意:有一个n个数的数组a,有n-1轮操作,每轮由每个数选择一个和它的差最大的数,如果相同就选值更大的,被最多数组选择的数字被删去,有相同的也去掉数值更大的那个…...

Nacos的搭建及服务调用
文章目录 一、搭建Nacos服务1、Nacos2、安装Nacos3、Docker安装Nacos 二、OpenFeign和Dubbo远程调用Nacos的服务1、搭建SpringCloudAlibaba的开发环境1.1 构建微服务聚合父工程1.2 创建子模块cloud-provider-payment80011.3 创建子模块cloud-consumer-order80 2、远程服务调用O…...

uniapp小程序自定义loding,通过状态管理配置全局使用
一、在项目中创建loding组件 在uniapp的components文件夹下创建loding组件,如图: 示例代码: <template><view class"loginLoading"><image src"../../static/loading.gif" class"loading-img&q…...

leetcode 45. 跳跃游戏 II
2023.7.30 class Solution { public:int jump(vector<int>& nums) {int step 0;int cover 0;int largest 0;if(nums.size() 1) return step;for(int i0; i<nums.size(); i){cover max(cover , inums[i]); //最大覆盖范围if(cover > nums.size()-1) retur…...