AQS是什么?AbstractQueuedSynchronizer之AQS原理及源码深度分析

news2024/11/24 16:08:56

文章目录

  • 一、AQS概述
    • 1、什么是AQS
    • 2、技术解释
    • 3、基本原理
    • 4、AQS为什么这么重要
  • 二、AQS数据结构
    • 1、AQS的结构
    • 2、ReentrantLock与AbstractQueuedSynchronizer
    • 3、AQS的state变量
    • 4、AQS的队列
    • 5、AQS的Node
      • (1)Node的waitStatus
      • (2)属性说明
  • 三、ReentrantLock的lock方法分析AQS源码
    • 1、类图
    • 2、FairSync和NonfairSync
    • 3、tryAcquire():尝试获取锁
    • 4、addWaiter():入队
    • 5、acquireQueued():队列线程挂起
    • 6、cancelAcquire():中断等异常导致的取消
  • 四、ReentrantLock的unlock方法分析AQS源码
    • 1、unlock
    • 2、tryRelease():释放锁
    • 3、unparkSuccessor():唤醒等待线程
  • 五、大总结
  • 参考资料

一、AQS概述

1、什么是AQS

AQS 的全称为(AbstractQueuedSynchronizer),这个类在 java.util.concurrent.locks 包下面。
在这里插入图片描述
其中,AbstractOwnableSynchronizer是AbstractQueuedLongSynchronizer和AbstractQueuedSynchronizer的父类。
在这里插入图片描述
其中AbstractOwnableSynchronizer和AbstractQueuedLongSynchronizer是jdk1.6引入的,AbstractQueuedSynchronizer是jdk1.5引入的。

2、技术解释

AQS是用来实现锁或者其他同步器组件的公共基础部分的抽象实现,是重量级基础框架及整个JUC体系的基石,主要用于解决锁分配给“谁”的问题。

AQS用于实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件等)。并通过一个int类型变量表示持有锁的状态。

如图所示,AQS利用CLH(Craig、Landin and Hagersten,是一个单向链表,但AQS中的队列是CLH变体的虚拟双向队列FIFO)队列 加上一个int类型的公共变量实现的。
在这里插入图片描述

AbstractQueuedLongSynchronizer:此类具有与完全相同 AbstractQueuedSynchronizer 的结构、属性和方法,不同之处在于所有与状态相关的参数和结果都定义为 long 而不是 int。在创建同步器(如需要 64 位状态的多级锁和屏障)时,此类可能很有用。AbstractQueuedLongSynchronizer并没有具体实现类,目前还是用的AbstractQueuedSynchronizer。

3、基本原理

抢到资源的线程直接使用处理业务逻辑,抢不到资源的线程必然需要排队等候,而存储这些排队等候的资源就是AQS中的队列(CLH队列的变体-双端队列)。

如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配,这个机制主要是CLH队列的变体来实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS同步队列的抽象实现。它将要请求共享资源的线程及自身的等待状态封装成队列的结点对象(Node),通过CAS、自选以及LockSupport.park()的方式,维护state变量的状态,使并发达到同步的效果。

4、AQS为什么这么重要

下图是AQS的众多实现类,我们发现,JUC中的许多非常重要的类,都有着AQS的影子:
在这里插入图片描述
我们平常用的ReentrantLock、Semaphore、CountDownLatch等等,这都是定义了程序员和锁交互的使用层API,隐藏了实现细节,使用就可以。而AQS就是统一规范并简化了锁的实现,将其抽象出来,屏蔽了同步状态管理、同步队列的管理和维护、阻塞线程排队和通知、唤醒机制等等。所以,AQS是一切锁和同步组件实现的公共基础部分

二、AQS数据结构

1、AQS的结构

AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成一个Node结点来实现锁的分配,通过CAS完成对state值的修改。
在这里插入图片描述
其中Node中也包含头指针和尾指针,也包含了一个重要的属性waitStatus,这里区别于AQS的state。
在这里插入图片描述

2、ReentrantLock与AbstractQueuedSynchronizer

在这里插入图片描述

3、AQS的state变量

该变量位0时意味着没有线程占用,大于等于1意味着有线程占用,其它线程需要等候。
在这里插入图片描述

4、AQS的队列

CLH:Craig、Landin and Hagersten,是一个单向链表,但AQS中的队列是CLH变体的虚拟双向队列FIFO。
其数据结构图如下:
在这里插入图片描述
官方图是单向链表,但是AQS实质是双端队列。
在这里插入图片描述

5、AQS的Node

Node分共享和独占模式

(1)Node的waitStatus

Node就是等候区的线程的存储单元,一个线程对应一个Node。

Node的等待状态waitStatus是一个int类型的变量(区别于AQS的stete)。总共有如下几种:

// 线程被取消了
static final int CANCELLED =  1;
// 后继线程需要唤醒
static final int SIGNAL    = -1;
// 等待condition唤醒
static final int CONDITION = -2;
// 共享或同步状态获取将会无条件地传播下去
static final int PROPAGATE = -3;

// 初始为0,状态是上面几种
volatile int waitStatus;

// 前驱结点
volatile Node prev;

// 后继节点
volatile Node next;

// 当前线程
volatile Thread thread;

(2)属性说明

在这里插入图片描述
其中waitStatus共有5个状态:
1.为0时,当第一个Node被初始化时的默认值。
2.为CANCELLED时(-1),表示线程获取锁的请求已经取消了。
3.为CONDITION(-2),表示节点在等待队列中,节点线程等待唤醒。
4.为PROPAGATE(-3),当前线程处在SHARED情况下,该字段才会使用。
5.SIGNAL(-1),表示线程已经准备好了,就等资源释放了。

三、ReentrantLock的lock方法分析AQS源码

1、类图

Lock接口的实现类,基本都是通过【聚合】了一个【队列同步器】的子类(Sync)完成线程访问控制的。
在这里插入图片描述

2、FairSync和NonfairSync

FairSync是公平锁,讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;
NonfairSync是非公平锁,不管是否有等待队列,如果可以获取锁,则立刻占有锁的对象。也就是说队列的第一个排队线程苏醒后,不一定就是排头的这个线程获取锁,它还是需要参加竞争(存在线程竞争的情况下),所以有可能出现后来的线程插队获取锁的现象。

它们的源码相差很小:
在这里插入图片描述
最终调用的acquire(1),就是AQS的核心方法:

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

3、tryAcquire():尝试获取锁

tryAcquire是AQS中定义的,但是由FairSync和NonfairSync重写:
在这里插入图片描述
我们分析NonfairSync:tryAcquire方法。

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}


final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState(); // 获取锁状态
    if (c == 0) { 
    	// 如果资源未被占用,compareAndSetState使用cas获取锁,设置锁状态
        if (compareAndSetState(0, acquires)) {
        	// 设置所属线程为当前线程
            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 + 1,仍然可以获取锁(可重入)
        return true;
    }
    return false;
}

所以,tryAcquire()是相当于做了一次尝试,尝试获取锁,如果获取锁那就皆大欢喜,获取不到就继续addWaiter(Node.EXCLUSIVE), arg)方法。

4、addWaiter():入队

// java.util.concurrent.locks.AbstractQueuedSynchronizer#addWaiter
// mode – Node.EXCLUSIVE 表示独占,Node.SHARED 表示共享
private Node addWaiter(Node mode) {
	// 一个Node包含一个当前线程的实例
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail; // 尾指针 ,初始为null
    if (pred != null) { // 不为null时
        node.prev = pred;
        if (compareAndSetTail(pred, node)) { // 设置尾指针为当前
            pred.next = node;
            return node;
        }
    }
    // 入队
    enq(node);
    return node;
}

// 将节点插入队列,必要时进行初始化
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; // 所有的Node形成一条链表
                return t;
            }
        }
    }
}

最终形成如下的数据结构:
在这里插入图片描述

5、acquireQueued():队列线程挂起

// arg = 1
// Node是当前节点
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
        	// predecessor获取前置节点
            final Node p = node.predecessor();
            // 如果是头结点,直接tryAcquire尝试获取锁
            if (p == head && tryAcquire(arg)) {
            	// 获取锁成功,将node设为头结点
                setHead(node);
                // 将头结点的关联取消
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 尝试获取锁失败 或者不是第一个节点
            // shouldParkAfterFailedAcquire : 设置前置节点的waitStatue0->-1,首次false,再次为true,再来个线程直接true
            if (shouldParkAfterFailedAcquire(p, node) &&
            	// LockSupport.park暂停当前线程
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	// 获取前驱结点的状态
    int ws = pred.waitStatus; // 0
    if (ws == Node.SIGNAL) // Node.SIGNAL :-1 即等待被占用的资源释放,直接返回true
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) { // > 0是CANCELLED状态,忽略改状态的节点,重新链接队列
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else { // 将当前节点的前驱结点设置为SIGNAL : -1,用于后续唤醒操作
        /*
         * 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); // 把前置节点的waitStatue 改为 -1
    }
    return false;
}

// 暂停
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this); // 线程挂起,等待被unpark、被中断
    return Thread.interrupted(); // 返回当前线程中断状态,并清空中断状态
}

6、cancelAcquire():中断等异常导致的取消

如果在自旋中,并没有获取锁,而是异常原因中断了,就会执行finally 中的cancelAcquire方法。

private void cancelAcquire(Node node) {
    // Ignore if node doesn't exist
    if (node == null)
        return;

    node.thread = null;

    // Skip cancelled predecessors
    Node pred = node.prev; // 前一个节点
    while (pred.waitStatus > 0) // CANCELLED:1,再找前一个节点
        node.prev = pred = pred.prev;

    // predNext is the apparent node to unsplice. CASes below will
    // fail if not, in which case, we lost race vs another cancel
    // or signal, so no further action is necessary.
    Node predNext = pred.next; // 下一个节点

    // Can use unconditional write instead of CAS here.
    // After this atomic step, other Nodes can skip past us.
    // Before, we are free of interference from other threads.
    node.waitStatus = Node.CANCELLED; // 本节点的waitStatue设置为CANCELLED

    // If we are the tail, remove ourselves.
    if (node == tail && compareAndSetTail(node, pred)) { // 如果本节点为尾结点,就将尾结点设为上一个不为CANCELLED的节点
        compareAndSetNext(pred, predNext, null); // 前一节点的next节点设为null
    } else { // 如果不是队尾
        // If successor needs signal, try to set pred's next-link
        // so it will get one. Otherwise wake it up to propagate.
        int ws;
        if (pred != head && // 上一个不为CANCELLED的节点不是head节点
            ((ws = pred.waitStatus) == Node.SIGNAL || // 上一个不为CANCELLED的节点waitStatus 为SIGNAL -1
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && 
            pred.thread != null) { // 上一个不为CANCELLED的节点 thread不为null
            Node next = node.next; // 本节点的next
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next); // 上一个不为CANCELLED的节点 的next 设为为本节点的next
        } else {
            unparkSuccessor(node); // 本节点unlock
        }

        node.next = node; // help GC
    }
}

四、ReentrantLock的unlock方法分析AQS源码

1、unlock

public void unlock() {
    sync.release(1);
}
public final boolean release(int arg) {
    if (tryRelease(arg)) { // 释放state为0
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h); // 唤醒
        return true;
    }
    return false;
}

2、tryRelease():释放锁

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;// 1 -1 = 0
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true; // 设置占用线程为null
        setExclusiveOwnerThread(null);
    }
    setState(c); state设为0
    return free;
}

3、unparkSuccessor():唤醒等待线程

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; // 头结点的waitStatue ,有线程占用的话为-1
    if (ws < 0) // 设为0
        compareAndSetWaitStatus(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; // 头结点的下一个节点
    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);
}

五、大总结

(图片模糊的话,保存下来本地看)
在这里插入图片描述

参考资料

https://zhuanlan.zhihu.com/p/378219920

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

UWB工业现场数字化管理

资产管理数字化 器具场内外流转跟踪管理 无动力资产跟踪 大件地堆及成品固定区域盘点 大型资产移动/流动盘点 成品及返修车定位 无人值守人、车、物出入监测 资产移动盘点 安全生产数字化 危险区域人员/物资管理 叉车防碰撞及安全运行管理 行车防碰撞及安全运行管理 …

SLAM从入门到精通(robot上层软件开发)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 前面我们了解到&#xff0c;ros系统基本上都是依赖于ubuntu和linux来运行的。如果ros是只是跑在机器人的系统上面&#xff0c;不涉及到上层交互&am…

LeetCode:1402. 做菜顺序、2316. 统计无向图中无法互相到达点对数

1. 1402 做菜顺序 题目详细为&#xff1a; 一个厨师收集了他 n 道菜的满意程度 satisfaction &#xff0c;这个厨师做出每道菜的时间都是 1 单位时间。 一道菜的 「 like-time 系数 」定义为烹饪这道菜结束的时间&#xff08;包含之前每道菜所花费的时间&#xff09;乘以这道菜…

外汇天眼:本周无牌裸奔平台名单出炉,你踩“坑”了么?!!

监管信息早知道&#xff01;外汇天眼将每周定期公布监管牌照状态发生变化的交易商&#xff0c;以供投资者参考&#xff0c;规避投资风险。如果平台天眼评分过高&#xff0c;建议投资者谨慎选择&#xff0c;因为在外汇天眼评分高不代表平台没问题&#xff01; 以下是监管牌照发生…

Gartner发布2024 年十大战略技术趋势

10月17日&#xff0c;Gartner 发布2024年企业机构需要探索的****十大战略技术趋势。Gartner研究副总裁Bart Willemsen表示&#xff1a;“由于技术变革以及社会经济方面的不确定性&#xff0c;我们必须大胆采取行动并从战略上提高弹性&#xff0c;而不是采取临时措施。IT领导者的…

塔望3W消费战略全案丨九代拉祜人,一饼古树茶

存木香 客户&#xff1a;云南双江存木香茶叶商贸有限公司 品牌&#xff1a;存木香 时间&#xff1a;2019年 &#xff08;项目部分内容保密期&#xff09; 沧海桑田 存木香依然 存木香 CUNMUXIANG( 全称云南双江存木香茶业有限公司 ), 成立于2011 年 , 总部设于北回归线横…

小红书内测「群AI」功能;大模型技术图谱;曾鸣「看十年」智能商业演讲实录;GPT最佳实践-大白话编译版 | ShowMeAI日报

&#x1f440;日报&周刊合集 | &#x1f3a1;生产力工具与行业应用大全 | &#x1f9e1; 点赞关注评论拜托啦&#xff01; &#x1f525; 小红书内测「群AI」功能&#xff0c;可以创建虚拟角色并加入群聊 小红书正在内测「群AI」新功能。日报根据站内分享贴拼出了一份教程&…

C++并发编程(1)-- 多线程的基础知识

1 进程、线程和协程 &#xff08;1&#xff09;进程 进程可以简单理解成正在执行的一个程序&#xff0c;比如你电脑上运行的QQ、360杀毒软件等就是进程。 进程是程序资源管理的最小单位。 进程会管理那些资源呢&#xff1f;通常包括内存资源、IO资源、信号处理等。 这里对…

众和策略:华为汽车概念活跃,圣龙股份斩获12板,华峰超纤涨10%

华为轿车概念23日盘中再度生动&#xff0c;到发稿&#xff0c;华峰超纤涨超10%&#xff0c;佛山照明、圣龙股份、隆基机械、银宝山新等涨停&#xff0c;赛力斯涨近6%。 值得注意的是&#xff0c;圣龙股份已接连12个交易日涨停。 昨日晚间&#xff0c;圣龙股份宣布前三季度成果…

Cisco交换机关于DHCP SNOOPING的配置指令

在Cisco交换机上配置DHCP Snooping&#xff08;DHCP欺骗防护&#xff09; DHCP Snooping的作用 DHCP Snooping是一项重要的网络安全功能&#xff0c;可用于维护网络的安全性和可靠性&#xff0c;减少潜在的网络问题&#xff0c;并提供日志和监控功能&#xff0c;以便网络管理…

【Servlet】实现Servlet程序

文章目录 1. 最朴素方式1. 创建项目2. 引入依赖3. 创建目录4. 编写代码5. 打包程序6. 部署程序7. 验证程序 2. 更方便方式1. 安装Smart TomCat插件2. 启动 1. 最朴素方式 1. 创建项目 选择Maven项目 2. 引入依赖 Maven项目创建完后会生成一个pom.xml文件&#xff0c;我们可…

rancher2.6.4配置管理k8s,docker安装

docker快速安装rancher并管理当前k8s集群。 1、拉镜像 docker pull rancher/rancher:v2.6.4 2、启动rancher 启动很慢 --privileged必须拥有root权限&#xff0c;并挂载卷 docker run --privileged -d --restartunless-stopped -p 80:80 -p 443:443 -v /usr/local/docker_vo…

rocketmq集群部署DLedger Controller

目录 rockermq集群部署DLedger Controller到官网下载rockermq集群模式&#xff1a;3切片主从建目录配置Nameserver先修改内存 启动NameServer停止NameServer配置Broker启动Broker启动管制台直接docker 启动即可&#xff0c;修改 -e 后面 nameserver 参数即可端口列表导入 rocke…

h5插件_h5页面嵌入客户端调试

当h5页面嵌入客户端之后&#xff0c;若是遇到问题无法调试&#xff0c;比如点击按钮无反应 —> 但是开发却看不到控制台、看不到接口返回值… 此时可以使用调试工具来查看… edura 引入1 <script src"https://cdn.jsdelivr.net/npm/eruda"></script&g…

向某文件中逐秒追加带序号输入当前时间 fgets fputs fprintf sprintf

//向某文件中逐秒追加带序号输入当前时间 #include<stdio.h> #include<stdlib.h> #include<time.h> #include<string.h> #include <unistd.h> int main(int argc, char const *argv[]) { time_t tv; // time(&tv);//法1:获取秒数 …

2023面试经典 Redis未授权漏洞与组合拳

文前漫谈 之前面试里碰见过&#xff0c;属于面试经典了&#xff0c;有空了了解一下 2015年的洞了&#xff0c;从以前乌云一个师傅的文章那了解到 Redis 有关的漏洞具有明显的时间分段性&#xff0c;在15年11月之前&#xff0c;主要是未授权导致的数据泄露&#xff0c;获得一些…

自增还是UUID,数据库主键的类型该如何选择?

一、自增(auto_increment)和UUID优缺点 自增 &#xff08;auto_increment&#xff09;的优点: 1.字段长度较uuid小很多&#xff0c;可以是bigint甚至是int类型&#xff0c;这对检索的性能会有所影响。 2.在写的方面&#xff0c;因为是自增的&#xff0c;所以主键是趋势自增的&…

小游戏外包开发流程及费用

小游戏的开发流程和费用会因项目的规模、复杂性和所选技术平台而有所不同。以下是一般的小游戏开发流程和可能的费用因素&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 开发流程&#xff1a; 概念和…

Android Studio Gradle中没有Task任务,没有Assemble任务,不能方便导出aar包

Gradle中&#xff0c;没有Assemble任务 1. 在编译aar包或者编译module的时候&#xff0c;没有release包&#xff0c;我们一般都是通过assemble进行编译。 如果在Gradle中找不到task。 可以通过设置File->setting -->Experimental→取消勾选“Do not build Gradle task …

TransactionScope的使用

TransactionScope的使用 简介1. 命名空间2.创建事务范围3.嵌套事务4.事务提交和回滚5.支持分布式事务6.配置选项7.资源管理器8.分布式事务协调器 应用1.未设置分布式事务2.设置分布式事务 简介 TransactionScope 是 .NET Framework 和 .NET Core 中的一个类&#xff0c;用于简化…