AQS(AbstractQueuedSynchronizer)队列同步器源码解读

🏷️个人主页:牵着猫散步的鼠鼠 

🏷️系列专栏:Java全栈-专栏

🏷️个人学习笔记,若有缺误,欢迎评论区指正 

目录

1. 前言

2. AOS、AQS、AQLS的区别

3. AQS的底层原理

3.1. 核心思想

3.2. 数据结构

3.2.1. state

AQS内部还提供了获取和修改state的方法,注意,这里的方法都是final修饰的,意味着不能被子类重写!

3.2.2 CLH 双向队列

3.3. 资源共享

3.3.1. 独占模式

3.3.2. 共享模式

3.4  Node节点

3.5. 获取资源与释放资源

3.5.1. 获取资源

3.5.2. 释放资源 

4.总结


1. 前言

AQS 的全称为 AbstractQueuedSynchronizer ,翻译过来的意思就是抽象队列同步器。这个类在 java.util.concurrent.locks 包下面。AQS为Java的并发包提供了强大的同步支持。通过内置的FIFO队列来完成资源获取线程的排队工作,并且利用一个被volatile关键字修饰的int类型的变量表示同步状态。AQS 为构建锁和同步器提供了一些通用功能的实现,许多同步类实现都依赖于它,如常用的ReentrantLock、Semaphore等。

2. AOS、AQS、AQLS的区别

AOS(AbstractOwnableSynchronizer) : JDK1.6时发布的,是AQS和AQLS的父类,这个类的主要作用是表示持有者与锁之间的关系。

AQS(AbstractQueuedSynchronizer) :JDK1.5时发布,抽象队列同步器,是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的同步器,诸如:ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue等等皆是基于 AQS 的。AQS 内部使用了一个 volatile 的变量 state(int类型) 来作为资源的标识。

AQLS(AbstractQueuedLongSynchronizer) :这个类诞生于JDK1.6,原因时上述的int类型的state资源,在当下的业务场景中,资源数量有可能超过int范围,因此,便诞生了这个类,采用Long类型的state。

//AQS中共享变量,使用volatile修饰保证线程可见性
private volatile int state;

//AQLS中共享变量,采用long类型
private volatile long state;

3. AQS的底层原理

以上我们大致的介绍了一下AQS是什么,其实在很多面试中都会提及AQS,可能被问到最多的就是:“麻烦介绍一下AQS的底层原理?”,很多同学都浅尝辄止,今天我就来总结下AQS的底层实现

3.1. 核心思想

核心思想大概如下:在多线程访问共享资源时,若标识的共享资源空闲,则将当前获取到共享资源的线程设置为有效工作线程,共享资源设置为锁定状态(独占模式下),其他线程没有获取到资源的线程进入阻塞队列,等待当前线程释放资源后,继续尝试获取。

3.2. 数据结构

其实AQS的实现主要基于两个内容,分别是 state 变量和基于FIFO结构的 CLH 双向队列

3.2.1. state

state 变量由 volatile 修饰,保证了多线程环境下的可见性,用于展示当前临界资源的获锁情况。

     /**
     * The synchronization state.
     */
    private volatile int state;

AQS内部还提供了获取和修改state的方法,注意,这里的方法都是final修饰的,意味着不能被子类重写!

    // 获取state
    protected final int getState() {
        return state;
    }

    // 修改state
    protected final void setState(int newState) {
        state = newState;
    }

    // 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);
    }
3.2.2 CLH 双向队列

我们在上面提到了独占模式下,没有获取资源的线程会被放入队列,然后阻塞、唤醒、锁的重分配机制,就是基于CLH实现的。CLH 锁 (Craig, Landin, and Hagersten locks)是一种自旋锁的改进,是一个虚拟的双向队列,所谓虚拟是指没有队列的实例,内部仅存各结点之间的关联关系。

AQS 将每条请求共享资源的线程封装成一个 CLH 队列锁的一个节点(Node)来实现锁的分配。在 CLH 队列锁中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。

3.3. 资源共享

在AQS的框架中对于资源的获取有两种方式:

  • 独占模式(Exclusive) :资源是独有的,每次只能一个线程获取,如ReentrantLock;
  • 共享模式(Share) :资源可同时被多个线程获取,具体可获取个数可通过参数设定,如CountDownLatch(我们后续也会出这个的工具类的源码解读),Semaphore信号量(上篇文章讲了)。
3.3.1. 独占模式

以ReentrantLock为例,其内部维护了一个state字段,用来标识锁的占用状态,初始值为0,当线程1调用lock()方法时,会尝试通过tryAcquire()方法(钩子方法)独占该锁,并将state值设置为1,如果方法返回值为true表示成功,false表示失败,失败后线程1被放入等待队列中(CLH队列),直到其他线程释放该锁。

但需要注意的是,在线程1获取到锁后,在释放锁之前,自身可以多次获取该锁,每获取一次state加1,这就是锁的可重入性,这也说明ReentrantLock是可重入锁,在多次获取锁后,释放时要释放相同的次数,这样才能保证最终state为0,让锁恢复到未锁定状态,其他线程去尝试获取!

3.3.2. 共享模式

CountDownLatch(倒计时器)就是基于AQS共享模式实现的同步类,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程开始执行任务,每执行完一个子线程,就调用一次 countDown() 方法。该方法会尝试使用 CAS(Compare and Swap) 操作,让 state 的值减少 1。当所有的子线程都执行完毕后(即 state 的值变为 0),CountDownLatch 会调用 unpark() 方法,唤醒主线程。这时,主线程就可以从 await() 方法(CountDownLatch 中的await() 方法而非 AQS 中的)返回,继续执行后续的操作。

Semaphore信号量也是基于基于AQS共享模式实现的同步类,也是通过CAS操作维护AQS内部的state值来记录许可证数,详细可见博文Semaphore信号量源码解读与使用-CSDN博客

3.4  Node节点

上述的两种共享模式、线程的引用、前驱节点、后继节点等都存储在Node对象中,我们接下来就走进Node的源码中一探究竟!

static final class Node {
    // 标记一个结点(对应的线程)在共享模式下等待
    static final Node SHARED = new Node();
    // 标记一个结点(对应的线程)在独占模式下等待
    static final Node EXCLUSIVE = null;

    // waitStatus的常量值,表示该结点(对应的线程)已被取消
    static final int CANCELLED = 1;
    // waitStatus的常量值,表示后继结点(对应的线程)需要被唤醒
    static final int SIGNAL = -1;
    // waitStatus的常量值,表示该结点(对应的线程)在等待某一条件
    static final int CONDITION = -2;
    /*waitStatus的常量值,表示有资源可用,新head结点需要继续唤醒后继结点(共享模式下,多线程并发释放资源,而head唤醒其后继结点后,需要把多出来的资源留给后面的结点;设置新的head结点时,会继续唤醒其后继结点)*/
    static final int PROPAGATE = -3;

    // 等待状态,取值范围,-3,-2,-1,0,1
    volatile int waitStatus;
    volatile Node prev; // 前驱结点
    volatile Node next; // 后继结点
    volatile Thread thread; // 结点对应的线程
    Node nextWaiter; // 等待队列里下一个等待条件的结点


    // 判断共享模式的方法
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }

    // 其它方法忽略,可以参考具体的源码
}

// AQS里面的addWaiter私有方法
private Node addWaiter(Node mode) {
    // 使用了Node的这个构造函数
    Node node = new Node(Thread.currentThread(), mode);
    // 其它代码省略
}

其中我们需要着重注意waitStatus这个变量,他可取以下五个值

  • CANCELLED:这个变量表示当前节点(对应的线程)已被取消。当等待超时或被中断,会触发进入为此状态,进入该状态后节点状态不再变化; 
  • SIGNAL: 后面节点等待当前节点唤醒; 
  • CONDITION: Condition 中使用,当前线程阻塞在Condition,如果其他线程调用了Condition的signal方法,这个节点将从等待队列转移到同步队列队尾,等待获取同步锁; 
  • PROPAGATE: 共享模式,前置节点唤醒后面节点后,唤醒操作无条件传播下去;
  • 0:中间状态,当前节点后面的节点已经唤醒,但是当前节点线程还没有执行完成。 

3.5. 获取资源与释放资源

有了以上的知识积累后,我们再来看一下AQS中关于获取资源和释放资源的实现吧。

3.5.1. 获取资源

在AQS中获取资源的是入口是acquire(int arg)方法,arg 是要获取的资源个数,在独占模式下始终为 1。

public final void accquire(int arg) {
    // tryAcquire 再次尝试获取锁资源,如果尝试成功,返回true,尝试失败返回false
    if (!tryAcquire(arg) &&
        // 走到这,代表获取锁资源失败,需要将当前线程封装成一个Node,追加到AQS的队列中
        //并将节点设置为独占模式下等待
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 线程中断
        selfInterrupt();
}

tryAcquire()是一个可被子类具体实现的钩子方法,用以在独占模式下获取锁资源,如果获取失败,则把线程封装为Node节点,存入等待队列中,实现方法是addWaiter(),我们继续跟入源码去看看。

private Node addWaiter(Node mode) {
    //创建 Node 类,并且设置 thread 为当前线程,设置为排它锁
    Node node = new Node(Thread.currentThread(), mode);
    // 获取 AQS 中队列的尾部节点
    Node pred = tail;
    // 如果 tail == null,说明是空队列,
    // 不为 null,说明现在队列中有数据,
    if (pred != null) {
        // 将当前节点的 prev 指向刚才的尾部节点,那么当前节点应该设置为尾部节点
        node.prev = pred;
        // CAS 将 tail 节点设置为当前节点
        if (compareAndSetTail(pred, node)) {
            // 将之前尾节点的 next 设置为当前节点
            pred.next = node;
            // 返回当前节点
            return node;
        }
    }
    enq(node);
    return node;
}

// 自旋CAS插入等待队列
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

在这部分源码中,将获取资源失败的线程封装后的Node节点存入队列尾部,考虑到多线程情况下的节点插入问题,这里提供了CAS的方式保证插入操作的原子性,通过自旋方式(一直循环,不让线程挂起,减少线程状态切换带来的开销)来减少性能损耗。

等待队列中的所有线程,依旧从头结点开始,一个个的尝试去获取共享资源,这部分的实现可以看acquireQueued()方法,我们继续跟入。

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        // interrupted用于记录线程是否被中断过
        boolean interrupted = false;
        for (;;) { // 自旋操作
            // 获取当前节点的前驱节点
            final Node p = node.predecessor();
            // 如果前驱节点是head节点,并且尝试获取同步状态成功
            if (p == head && tryAcquire(arg)) {
                // 设置当前节点为head节点
                setHead(node);
                // 前驱节点的next引用设为null,这时节点被独立,垃圾回收器回收该节点
                p.next = null; 
                // 获取同步状态成功,将failed设为false
                failed = false;
                // 返回线程是否被中断过
                return interrupted;
            }
            // 如果应该让当前线程阻塞并且线程在阻塞时被中断,则将interrupted设为true
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 如果获取同步状态失败,取消尝试获取同步状态
        if (failed)
            cancelAcquire(node);
    }
}

在这个方法中,从等待队列的head节点开始,循环向后尝试获取资源,获取失败则继续阻塞,头结点若获取资源成功,则将后继结点设置为头结点,原头结点从队列中回收掉 ,这里我们就可以发现堵塞队列中线程中的唤醒是有序的(上次面试被问到是有序还是无序,好在这一块记忆比较深刻)

如果是无序唤醒,会引起羊群效应,多个线程在竞争state资源,竞争锁的过程中,就会造成资源的浪费。

3.5.2. 释放资源 

相对于获取资源,AQS中的资源释放就简单多啦,我们直接上源码!

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

private void unparkSuccessor(Node node) {
    // 如果状态是负数,尝试把它设置为0
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    // 得到头结点的后继结点head.next
    Node s = node.next;
    // 如果这个后继结点为空或者状态大于0
    // 通过前面的定义我们知道,大于0只有一种可能,就是这个结点已被取消(只有 Node.CANCELLED(=1) 这一种状态大于0)
    if (s == null || s.waitStatus > 0) {
        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);
}

这里的tryRelease(arg)通过是个钩子方法,需要子类自己去实现,比如在ReentrantLock中的实现,会去做state的减少操作int c = getState() - releases;,毕竟这是一个可重入锁,直到state的值减少为0,表示锁释放完毕!

接下来会检查队列的头结点。如果头结点存在并且waitStatus不为0,这意味着还有线程在等待,那么会调用unparkSuccessor(Node h)方法来唤醒后续等待的线程。

4.总结

AQS (AbstractQueuedSynchronizer) 是抽象队列同步器,是Java并发包的根基,像Java的Lock,信号量(Semaphore)都用到了AQS。

AQS提供了独占模式和共享模式两种方式获取共享资源,其中ReentrantLock就用到独占模式实现了排他锁,CountDownLatch和Semaphore都用到了AQS的共享模式实现了共享锁。

AQS内部通过维护一个 volatile 修饰的state变量作为竞态条件,以及一个基于FiFO结构的CLH 双向队列队列来存放需要等待获取锁的线程。 多个线程通过对state这个变量修改来实现竞态条件,竞争失败的线程会加入等待队列并阻塞,当锁释放后会有序唤醒队列中的线程进行锁竞争。

我们在后续可能会基于AQS实现一个同步器,将AQS用起来,感兴趣的话就多多支持吧

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

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

相关文章

Qt 项目文件(.pro)概述

Qt 项目pro文件 引言一、pro文件初探二、部分参数详解 引言 Qt工程项目由项目文件&#xff08;.pro&#xff09;进行管理。qmake使用文件中的信息生成Makefile&#xff0c;其中包含构建每个项目所需的所有命令。pro文件通常包含源文件和头文件的列表、常规配置信息以及任何特定…

ST-GCN模型详解(+openpose)

ST-GCN模型详解&#xff08;openpose&#xff09; 一、什么是ST-GCN呢 基于骨架的动作识别&#xff08;Skeleton-Based Action Recognition&#xff09;主要任务是从一系列时间连续的骨骼关键点&#xff08;2D/3D&#xff09;中识别出正在执行的动作。因为牵涉到骨骼框架这种…

CentOS7升级openssl

文章目录 一 系统环境二 操作步骤三 版本检查 一 系统环境 公司服务器等保要求&#xff0c;修复openssl的高危漏洞。 本机使用centos7.9系统&#xff0c;openssl版本是1.0.2k&#xff0c;计划升级到1.1.1q 在执行下列操作前&#xff0c;务必要打快照做好备份&#xff0c;以防升…

030——从GUI->Client->Server->driver实现对红外遥控的控制

目录 1、 解决韦东山老师irda模块中断申请失败的bug 2、 client添加处理程序 3、 添加服务器处理程序和驱动处理句柄 4、 处理数据读出不准确问题 5、 修改后的展示 1、 解决韦东山老师irda模块中断申请失败的bug irda需要通过中断来触发读操作&#xff0c;申请中断需要引…

Octopus v2:斯坦福的嵌入设备专用大模型

斯坦福大学推出了 Octopus v2&#xff0c;这是一种突破性的设备上语言模型&#xff0c;旨在解决与现有模型相关的延迟、准确性和隐私问题。 NSDT工具推荐&#xff1a; Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑…

源码篇--Nacos服务--中章(1):Nacos服务端的启动

文章目录 前言一、Nacos Console 启动入口&#xff1a;二、启动过程&#xff1a;2.1 容器启动监听器&#xff1a;2.1.1 调整启动标识为正在启动状态&#xff1a;2.1.2 环境准备阶段&#xff1a;2.1.3 容器环境准备&#xff1a;2.1.4 自定义的环境变量 设置&#xff1a;2.1.5 服…

Spectre-v2 以及 Linux Retpoline技术简介

文章目录 前言一、Executive Summary1.1 Spectre-v2: Branch Predictor Poisoning1.2 Mitigating Spectre-v2 with Retpolines1.3 Retpoline Concept 二、BackgroundExploit Composition 三、(Un-)Directing Speculative Execution四、Construction (x86)4.1 Speculation Barri…

测试人员通常遇到的“坑”

网上看到一个帖子&#xff0c;从事多年的测试从业者&#xff0c;吐槽测试过程中遇到的“坑”&#xff0c;感觉比较有意思&#xff0c;我在工作当中也遇到通常的问题&#xff0c;看得出这位网友比较喜欢总结&#xff0c;帖子地址奉上&#xff0c;有兴趣的可以浏览一下&#xff1…

bug(警告):[vue-router] Duplicate named routes definition: …

查看警告&#xff1a;[vue-router] Duplicate named routes definition——翻译[vue-router]重复命名路由定义 小编劝诫&#xff1a;当我们在开发过程中警告也一定不要忽略&#xff0c;虽然你在本地跑代码时这些警告影响项目的正常运行&#xff0c;但是会让你产生误区&#xff…

大模型日报|今日必读的8篇大模型论文

大家好&#xff0c;今日必读的大模型论文来啦&#xff01; 1.EdgeFusion&#xff1a;端侧文本到图像生成&#xff0c;只需不到一秒 用于文本到图像生成的稳定扩散&#xff08;SD&#xff09;技术需要大量计算&#xff0c;这对其实际应用构成了重大障碍。为此&#xff0c;最近…

Oracle進階SQLDay03

一、函數進階復習 1、行轉列 select 用水儿量&#xff08;噸&#xff09; 统计项, sum(case when t_account.month01 then USENUM end) 一月, sum(case when t_account.month02 then USENUM end) 二月, sum(case when t_account.month03 then USENUM end) 三月, sum(case when …

STM32学习和实践笔记(15):STM32中断系统

中断概念 CPU执行程序时&#xff0c;由于发生了某种随机的事件(外部或内部)&#xff0c;引起CPU暂 时中断正在运行的程序&#xff0c;转去执行一段特殊的服务程序(中断服务子程序 或中断处理程序)&#xff0c;以处理该事件&#xff0c;该事件处理完后又返回被中断的程序 继…

飞桨Ai(二)paddle使用CPU版本可以正常识别,切换为GPU版本时无法识别结果

一、问题描述&#xff1a; 刚开始用paddle的CPU版本&#xff0c;对训练好的模型进行推理&#xff0c;正常识别出想要的结果后来尝试使用paddle的GPU版本&#xff0c;然后发现识别出来是空的 二、系统思路&#xff1a; 最终系统环境如下&#xff1a; 系统&#xff1a;win10 …

有哪些公认好用且免费的云渲染网渲平台?渲染100邀请码1a12

现在云渲染是越来越火了&#xff0c;无论是在建筑设计、影视动画还是效果图行业都有它的身影&#xff0c;云渲染能缩短制作周期&#xff0c;提高工作效率&#xff0c;那么市面上有哪些公认好用且免费的云渲染平台呢&#xff1f;这次我们来了解下。 首先&#xff0c;我们来看看有…

vulfocus靶场tomcat-cve_2017_12615 文件上传

7.0.0-7.0.81 影响版本 Windows上的Apache Tomcat如果开启PUT方法(默认关闭)&#xff0c;则存在此漏洞&#xff0c;攻击者可以利用该漏洞上传JSP文件&#xff0c;从而导致远程代码执行。 Tomcat 是一个小型的轻量级应用服务器&#xff0c;在中小型系统和并发访问用户不是很多…

「GO基础」在Windows上配置VS Code GO语言开发环境

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

查看linux的主机配置脚本

废话不说 直接上指令 curl -Lso- bench.sh | bash 等待后&#xff0c;结果如图&#xff1a; 使用后没有问题&#xff0c;看情况使用 出事概不负责 介意勿用&#xff01;&#xff01;&#xff01;

LD-Pruner、EdgeFusion(On-Device T2I)、FreeDiff、TextCenGen、MemLLM

本文首发于公众号&#xff1a;机器感知 https://mp.weixin.qq.com/s/KiyNfwYWU-wBiCO-hE9qkA 苏 The devil is in the object boundary: towards annotation-free instance segmentation using Foundation Models Foundation models, pre-trained on a large amount of data…

Windows系统下安装paddle

开始使用_飞桨-源于产业实践的开源深度学习平台 (paddlepaddle.org.cn) 命令行下&#xff1a; python -m pip install --upgrade pip --user python -m pip install paddlepaddle2.6.1 -i https://pypi.tuna.tsinghua.edu.cn/simple 报异常 ERROR: Could not install packa…

Jmeter 测试Dubbo接口-实例

1、Dubbo插件准备 ①把jmeter-plugins-dubbo-2.7.4.1-jar-with-dependencies.jar包放在D:\apache-jmeter-5.5\lib\ext目录 ②重新打开Jmeter客户端 在线程组-添加-取样器-dubbo simple&#xff0c;添加dubbo接口请求 2、Jmeter测试lottery接口 ①配置zookeeper参数 由于dub…