一文让你彻底搞懂AQS(通俗易懂的AQS)

article/2025/10/7 19:18:55

一文让你彻底搞懂AQS(通俗易懂的AQS)

一、什么是AQS

  • AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。

二、前置知识

  • 学习AQS需要大家对同步锁有一定的概念。同时大家要知道LockSupport的使用,可以参考我这篇文章。(LockSupport从入门到深入理解)

三、AQS 的核心思想

  • AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。 CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。 AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。 (图一为节点关系图)
private volatile int state;//共享变量,使用volatile修饰保证线程可见性

在这里插入图片描述

四、AQS 案例分析

上面讲述的原理还是太抽象了,那我我们上示例,结合案例来分析AQS 同步器的原理。以ReentrantLock使用方式为例。
代码如下:
public class AQSDemo {private static int num;public static void main(String[] args) {ReentrantLock lock = new ReentrantLock();new Thread(new Runnable() {@Overridepublic void run() {lock.lock();try {Thread.sleep(1000);num += 1000;System.out.println("A 线程执行了1秒,num = "+ num);} catch (InterruptedException e) {e.printStackTrace();}finally {lock.unlock();}}},"A").start();new Thread(new Runnable() {@Overridepublic void run() {lock.lock();try {Thread.sleep(500);num += 500;System.out.println("B 线程执行了0.5秒,num = "+ num);} catch (InterruptedException e) {e.printStackTrace();}finally {lock.unlock();}}},"B").start();new Thread(new Runnable() {@Overridepublic void run() {lock.lock();try {Thread.sleep(100);num += 100;System.out.println("C 线程执行了0.1秒,num = "+ num);} catch (InterruptedException e) {e.printStackTrace();}finally {lock.unlock();}}},"C").start();}
}

执行的某一种结果! 这个代码超级简单,但是执行结果却是可能不一样,大家可以自行实验。
结果一
在这里插入图片描述
在这里插入图片描述
对比一下三种结果,大家会发现,无论什么样的结果,num最终的值总是1600,这说明我们加锁是成功的。

五、AQS 源码分析

  • 使用方法很简单,线程操纵资源类就行。主要方法有两个lock() 和unlock().我们深入代码去理解。我在源码的基础上加注释,希望大家也跟着调试源码。其实非常简单。

5.1 AQS 的数据结构

AQS 主要有三大属性分别是 head ,tail, state,其中state 表示同步状态,head为等待队列的头结点,tail 指向队列的尾节点。
    /*** Head of the wait queue, lazily initialized.  Except for* initialization, it is modified only via method setHead.  Note:* If head exists, its waitStatus is guaranteed not to be* CANCELLED.*/private transient volatile Node head;/*** Tail of the wait queue, lazily initialized.  Modified only via* method enq to add new wait node.*/private transient volatile Node tail;/*** The synchronization state.*/private volatile int state;
 还需要再去了解 Node的数据结构,
在这里插入代码片
class Node{//节点等待状态volatile int waitStatus;// 双向链表当前节点前节点volatile Node prev;// 下一个节点volatile Node next;// 当前节点存放的线程volatile Thread thread;// condition条件等待的下一个节点Node nextWaiter;
}

waitStatus 只有特定的几个常量,相应的值解释如下:
在这里插入图片描述
本次源码讲解,我们一ReentranLock的非公平锁为例。我们主要关注的方法是lock(),和unlock()。

5.2 lock源码分析

首先我们看一下lock()方法源代码,直接进入非公平锁的lock方法:

final void lock() {//1、判断当前state 状态, 没有锁则当前线程抢占锁if (compareAndSetState(0, 1))// 独占锁setExclusiveOwnerThread(Thread.currentThread());else// 2、锁被人占了,尝试获取锁,关键方法了acquire(1);}

进入 AQS的acquire() 方法:

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

总-分-总

  • lock方法主要由tryAquire()尝试获取锁,addWaiter(Node.EXCLUSIVE) 加入等待队列,acquireQueued(node,arg)等待队列尝试获取锁。示意图如下:
    lock方法整体流程

5.2.1 tryAquire 方法源码

  • 既然是非公平锁,那么我们一进来就想着去抢锁,不管三七二一,直接试试能不能抢到,抢不到再进队列。
  final boolean nonfairTryAcquire(int acquires) {//1、获取当前线程final Thread current = Thread.currentThread();// 2、获取当前锁的状态,0 表示没有被线程占有,>0 表示锁被别的线程占有int c = getState();// 3、如果锁没有被线程占有if (c == 0) {// 3.1、 使用CAS去获取锁,   为什么用case呢,防止在获取c之后 c的状态被修改了,保证原子性if (compareAndSetState(0, acquires)) {// 3.2、设置独占锁setExclusiveOwnerThread(current);// 3.3、当前线程获取到锁后,直接发挥truereturn true;}}// 4、判断当前占有锁的线程是不是自己else if (current == getExclusiveOwnerThread()) {// 4.1 可重入锁,加+1int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");// 4.2 设置锁的状态setState(nextc);return true;}return false;}

5.2.2 addWaiter() 方法的解析

  • private Node addWaiter(Node mode),当前线程没有货得锁的情况下,进入CLH队列。
 private Node addWaiter(Node mode) {// 1、初始化当前线程节点,虚拟节点Node node = new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failure// 2、获取尾节点,初始进入节点是nullNode pred = tail;// 3、如果尾节点不为null,怎将当前线程节点放到队列尾部,并返回当前节点if (pred != null) {node.prev = pred;if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}// 如果尾节点为null(其实是链表没有初始化),怎进入enq方法enq(node);return node;}// 这个方法可以认为是初始化链表private Node enq(final Node node) {// 1、入队 : 为什么要用循环呢?  for (;;) {// 获取尾节点Node t = tail;// 2、尾节点为nullif (t == null) { // Must initialize// 2.1 初始话头结点和尾节点if (compareAndSetHead(new Node()))tail = head;} // 3、将当前节点加入链表尾部else {node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}}

有人想明白为什么enq要用for(;;)吗? 咋一看最多只要循环2次啊! 答疑来了,这是对于单线程来说确实是这样的,但是对于多线程来说,有可能在第2部完成之后就被别的线程先执行入链表了,这时候第3步cas之后发现不成功了,怎么办?只能再一次循环去尝试加入链表,直到成功为止。

5.2.3 acquireQueued()方法详解

  • addWaiter 方法我们已经将没有获取锁的线程放在了等待链表中,但是这些线程并没有处于等待状态。acquireQueued的作用就是将线程设置为等待状态。
 final boolean acquireQueued(final Node node, int arg) {// 失败标识boolean failed = true;try {// 中断标识boolean interrupted = false;for (;;) {// 获取当前节点的前一个节点final Node p = node.predecessor();// 1、如果前节点是头结点,那么去尝试获取锁if (p == head && tryAcquire(arg)) {// 重置头结点setHead(node);p.next = null; // help GC// 获得锁failed = false;// 返回false,节点获得锁,,,然后现在只有自己一个线程了这个时候就会自己唤醒自己// 使用的是acquire中的selfInterrupt(); return interrupted;}// 2、如果线程没有获得锁,且节点waitStatus=0,shouldParkAfterFailedAcquire并将节点的waitStatus赋值为-1//parkAndCheckInterrupt将线程park,进入等待模式,if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}}private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus;if (ws == Node.SIGNAL)/** This node has already set status asking a release* to signal it, so it can safely park.*/return true;if (ws > 0) {/** Predecessor was cancelled. Skip over predecessors and* indicate retry.*/do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {/** waitStatus must be 0 or PROPAGATE.  Indicate that we* need a signal, but don't park yet.  Caller will need to* retry to make sure it cannot acquire before parking.*/compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;}
  • 好了,这个源码的解释就结束了,大家是不是还是云里雾里,不得不承认,这个代码太优雅了。不愧大神!

我用白话给大家串起来讲一下吧! 我们以reentrantLock的非公平锁结合我们案例4来讲解。
当线程A 到lock()方法时,通过compareAndSetState(0,1)获得锁,并且获得独占锁。当B,C线程去争抢锁时,运行到acquire(1),C线程运行tryAcquire(1),接着运行nonfairTryAcquire(1)方法,未获取锁,最后返回false,运行addWaiter(),运行enq(node),初始化head节点,同时C进入队列;再进入acquireQueued(node,1)方法,初始化waitStatus= -1,自旋并park()进入等待。
接着B线程开始去抢锁,B线程运行tryAcquire(1),运行nonfairTryAcquire(1)方法,未获得锁最后返回false,运行addWaiter(),直接添加到队尾,同时B进入队列;在进入acquireQueued(node,1)方法,初始化waitStatus= -1,自旋并park()进入等待。

AQS 核心方法流程图

5.3 unlock源码分析

unlock释放锁。主要利用的是LockSupport

  public final boolean release(int arg) {// 如果成功释放独占锁,if (tryRelease(arg)) {Node h = head;// 如果头结点不为null,且后续有入队结点if (h != null && h.waitStatus != 0)//释放当前线程,并激活等待队里的第一个有效节点unparkSuccessor(h);return true;}return false;}// 如果释放锁着返回true,否者返回false// 并且将sate 设置为0protected final boolean tryRelease(int releases) {int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;}private void unparkSuccessor(Node node) {/** If status is negative (i.e., possibly needing signal) try* to clear in anticipation of signalling.  It is OK if this* fails or if status is changed by waiting thread.*/int ws = node.waitStatus;if (ws < 0)// 重置头结点的状态waitStatuscompareAndSetWaitStatus(node, ws, 0);/** Thread to unpark is held in successor, which is normally* just the next node.  But if cancelled or apparently null,* traverse backwards from tail to find the actual* non-cancelled successor.*/// 获取头结点的下一个节点Node s = node.next;// s.waitStatus > 0 为取消状态 ,结点为空且被取消if (s == null || s.waitStatus > 0) {s = null;// 获取队列里没有cancel的最前面的节点for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}// 如果节点s不为null,则获得锁if (s != null)LockSupport.unpark(s.thread);}

锁的释放这个还是很简单。

总结

这个源码的最好阅读方式是结合例子去自己一步步跟代码,把每一个步骤写在纸上,尝试一两遍你就会有非常清晰的认识。

大家多给些意见,写之前我信心满满觉得能写的让大家看懂,写完之后我觉得一坨屎。


http://chatgpt.dhexx.cn/article/vKkk0soj.shtml

相关文章

什么是AQS

AQS ( Abstract Queued Synchronizer &#xff09;是一个抽象的队列同步器&#xff0c;通过维护一个共享资源状态&#xff08; Volatile Int State &#xff09;和一个先进先出&#xff08; FIFO &#xff09;的线程等待队列来实现一个多线程访问共享资源的同步框架。 一、AQS…

泛函分析之变分法

泛函数 以上截图来自于《变分法简介Part 1.&#xff08;Calculus of Variations&#xff09;》 变分法 研究泛函极值的方法就是所谓变分法。 以上截图来自于《最速降线的数学模型—变分法》 欧拉-拉格朗日方程

mathematica变分法和样条插值求解最小旋转曲面

mathematica求解最小面积旋转曲面 做你没做过的事叫成长&#xff0c;做你不愿做做的事叫改变&#xff0c;做你不敢做的事叫突破。—— 巴菲特 问题描述&#xff1a; 在一条直线的同一侧有两个已知点&#xff0c;试找出一条连接这两点的曲线&#xff0c;使这条曲线绕直线旋转所…

变分法模型的运用:生产设备的最大经济效益

上一节介绍了 动态优化模型/ 变分法 的基本思想&#xff0c;本节将一个变分法的运用。 目录 1 问题分析与假设 2 模型构造 3 模型求解 变分法习题 某工厂购买了一台新设备投入到生产中。一方面该设备随着运行时间的推…

简述变分法在泛函极值问题中的应用

此文主要有两部分内容&#xff0c;一部分是泛函的一些基本概念&#xff1b;第二部分是变分法在研究泛函极值问题中的应用。 第一部分 泛函 泛函是函数概念的一种扩充&#xff0c;函数描述的是从数到数的对应关系&#xff0c;从自变量到因变量的一种对应关系&#xff1b;而泛函…

变分法(欧拉 - 拉格朗日)和梯度下降求泛函最优解

泛函的简单理解&#xff1a; 是的变量&#xff0c; 这样的就叫泛函 . 加个积分&#xff0c;这样的就叫积分泛函 . 欧拉 - 拉格朗日 (E - L) 公式&#xff1a; 定义一个能量泛函如下&#xff1a; 我们的目的是找到能使 取到极值的时候 的取值&#xff0c;所以我们就假设 就…

第二章-最优控制中的变分法(经典变分法或古典变分法)1

是《最优控制理论与应用(邵克勇&#xff0c;王婷婷&#xff0c;宋金波)》的读书笔记&#xff0c;相比于其他的书&#xff0c;选择这本书的理由是页数少&#xff0c;能读完。解学书的《最优控制理论与应用》看目录感觉很全&#xff0c;但是太厚了&#xff0c;感觉看不完。 虽然…

变分法理解1——泛函简介

变分法是处理泛函的数学领域&#xff0c;和处理函数的传统微积分相对。 对泛函求极值的问题称为变分问题&#xff0c;使泛函取极值的函数称为变分问题的解&#xff0c;也称为极值函数。 传统的微积分中的一个常见的问题是找到一个 x x x 值使得 y ( x ) y(x) y(x) 取得最大值…

变分法证明两点之间线段最短

传送门https://zhuanlan.zhihu.com/yueaptx 变分法简介Part 1.&#xff08;Calculus of Variations&#xff09; Dr.Stein 计算力学 ​关注他 283 人赞了该文章 泛函数 (Functionals) 简而言之&#xff0c;泛函数是函数的函数&#xff0c;即它的输入是函数&#xff0c;输出…

最优控制理论 一、变分法和泛函极值问题

变分法是最优控制问题的三大基石之一&#xff0c;下面讨论一些变分法的常用理论。 1. 性能指标泛函 无约束最优控制问题&#xff0c;若固定起止时间&#xff0c;两端状态固定&#xff0c;即 x ( 0 ) x 0 , x ( t f ) x f , t ∈ [ 0 , t f ] x(0)x_0, x(t_f)x_f, t\in[0,t…

[变分法介绍]优美的旋轮线:最速下降线问题,通过费马光学原理的初等证明

[变分法介绍]优美的旋轮线:最速下降线问题,通过费马光学原理的初等证明 变分法 费马光学原理最速下降线问题旋轮线旋轮线最速下降性质的证明一些旋轮线及变形参考书目:1696年约翰伯努利在写给他哥哥雅克布伯努利的一封公开信中提出了如下的“捷线”问题:设想一个质点沿连接…

深入浅出解析变分法——一种常用的数学方法

前言&#xff1a;笔者从事图像处理行业&#xff0c;总是接触到变分法这个概念&#xff0c;一直没有很深入的去理解这个概念&#xff0c;同时我看其他大佬的博文也比较糊涂&#xff0c;因此最近花了一些时间好好梳理了这部分数学知识。文章共3部分&#xff0c;主要是对变分法的解…

变分法:在图像处理中的应用(一)

前言 最近学习稠密重建的相关知识&#xff0c;发现变分法通常作为一个平滑的正则项出现在残差平方和的损失函数中。而图像处理中又经常出现这类最小损失函数的优化问题&#xff0c;如图像分割、稠密光流、稠密重建等等&#xff0c;这些优化问题中都有可能涉及到变分法。因此&am…

电磁仿真原理——3. 变分法(Variationl Methods)

目录 引言线性空间的算子问题变分的计算问题欧拉公式 构造泛函的方法利用分部积分构造利用标准变分原理构造 瑞利一里茨法加权留数法本征问题变分的实际应用 引言 由于课程后面重点的矩量法和有限元法都是基于变分法进行的&#xff0c;变分法是它们的数学基础&#xff0c;实际…

泛函极值问题与变分法

泛函与泛函极值问题 平面内两点A&#xff0c;B&#xff0c;连接两点之间的曲线有很多种方式。分别用函数 f i ( x ) f_{i}(x) fi​(x)来表示。对于给定的曲线 f i ( x ) f_{i}(x) fi​(x), 那么两点之间连线的长度可以表示为 J ( f i ( x ) ) ∫ A B 1 f i ′ ( x ) 2 d x J…

【Matlab】变分法求控制器(无约束)

在动态最优控制中&#xff0c;目标函数是一个泛函数&#xff0c;求解动态最优化问题可以看做是求泛函极值的问题&#xff0c;求解泛函极值有一个方法&#xff0c;即变分法&#xff0c;本文章便介绍有关变分法的一些自己的学习理解。 变分法的基本概念 泛函 如果一个因变量的…

关于变分法

在介绍变分贝叶斯之前&#xff0c;首先以这篇博客介绍下大名鼎鼎的变分法。 参考资料主要是知乎的文章与维基百科。 变分就是函数的微分。 回顾一下传统的函数优化问题。 对于 min ⁡ x f ( x ) \min_x f(x) minx​f(x)这样的优化问题&#xff0c;求取最优的 x x x的做法常用…

变分法

变分法 弦平衡方程的导出&#xff0c;建立起横向位移u&#xff0c;张力T&#xff0c;外力f之间的关系&#xff1a; 方一、根据受力平衡导出 推导时用的技巧或假设&#xff1a; 1.泰勒展开近似 同理 2. 3.小变形假设&#xff0c;张力均匀&#xff0c;即 4.方程推导中忽略二…

变分法入门介绍

文章目录 变分法入门介绍泛函和变分法变分法求泛函极值变分的定义拉格朗日函数欧拉方程 案例分析--两点之间直线最短在Mathematica中使用变分法参考文献 变分法入门介绍 读完这篇博文你可以了解变分的基本概念&#xff0c;以及使用变分法求解最简泛函的极值。本文没有严密的数…

能量原理和变分法笔记1:变分法简介

上个学期在学校学了多体系统动力学的课&#xff0c;其中老师讲了变分原理&#xff0c;觉得很有启发&#xff0c;决定再学学相关的知识&#xff0c;在B站找到了一个这样的视频能量原理与变分法&#xff0c;做点笔记&#xff0c;加深一下理解。 第0章序言-微元、功和能(P2) 第1章…