JUC AQS(AbstractQueuedSynchronizer)

news2025/5/25 11:46:42

文章目录

  • AQS (AbstractQueuedSynchronizer^1.5+^)
  • CLH 锁队列
    • AbstractQueuedSynchronizer 成员变量说明
    • AbstractQueuedSynchronizer.Node 源码
    • CLH 队列原理图
      • 入队逻辑方法
      • 出队逻辑方法
  • 继承 AQS 需要实现的几个方法
  • AQS 对象序列化
  • ReentrantLock 源码解析
    • ReentrantLock Lock 加锁过程源码解析
    • ReentrantLock unlock 解锁过程源码解析
  • 总结

AQS (AbstractQueuedSynchronizer1.5+

按照类名直译过来就是抽象队列同步器。用于实现依赖先进先出(FIFO)等待队列的阻塞式锁或相关的同步器。它是这些锁、同步器的基础,提供了一个整体框架实现,我们只需要实现 AQS 的几个方法就能实现自己的 ReentrantLock

CLH 锁队列

CLH(Craig, Landin, Hagersten 三位大神发明的队列,其本身是一个单向链表,但在 AQS 中是使用的双向链表实现),它是 AQS 实现等待队列的基础

AbstractQueuedSynchronizer 成员变量说明


    /**
     * 等待队列的头,懒初始化的,在初始化后,只能通过 setHead 方法修改
     * 如果 head 节点的存在,则 head 的状态 waitStatus 不会是 Node.CANCELLED 的
     * 出队,就是从 head 出
     */
    private transient volatile Node head;

    /**
     * 等待队列的尾部,懒初始化,初始化后只能通过 enq 方法添加新的等待节点
     */
    private transient volatile Node tail;

    /**
     * 同步器的状态
     * 通过 getState、setState、compareAndSetState 方法操作
     * 
     */
    private volatile int state;

AbstractQueuedSynchronizer.Node 源码

static final class Node {
        /** 标识节点为共享模式 */
        static final Node SHARED = new Node();
        /** 标识节点为独占模式 */
        static final Node EXCLUSIVE = null;

        /** waitStatus 的值为1,标明为当前节点已取消 */
        static final int CANCELLED =  1;
        /** waitStatus 的值为 -1,标明当前节点的后续节点需要取消等待(调用unpark)*/
        static final int SIGNAL    = -1;
        /** waitStatus 的值为 -2,标明后续节点在 condition 上等待*/
        static final int CONDITION = -2;
        /**
         * waitStatus 的值为 -3,通知后续节点当前节点当前已 releaseShared 
         */
        static final int PROPAGATE = -3;

        /**
         * 状态字段,取值如下:
         *   SIGNAL -1:     此节点之后的节点为阻塞(park),当此节点在释放后,后续的节点应该upark
         *   CANCELLED 1:  此节点由于超时或中断而被取消,具有取消节点的线程永远不会再阻塞。
         *   CONDITION -2:  此节点当前位于条件队列中。在传输之前,它不会被用作同步队列节点,此时状态将被设置为0。 
         *   PROPAGATE -3:  需要保证传播 releaseShared 的其他节点
         *   0:          以上状态之外的值
         *
         * 非负数字不需要发送信号
         *
         * 普通同步节点初始化为 0
         * 条件节点初始化为 CONDITION
         */
        volatile int waitStatus;

        /**
         * 当前节点的前一个节点,在出队时被设置为 null
         */
        volatile Node prev;

        /**
         * 当前节点的 next 节点
         */
        volatile Node next;

        /**
         * 进入该节点的线程
         */
        volatile Thread thread;

        /**
         * 下一个等待条件的节点或特殊节点 SHARED
         */
        Node nextWaiter;

        /**
         * 判断节点是否在共享模式下等待
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        /**
         * 返回 prev 节点,如果为空,这抛出 NullPointerException 
         *
         * @return the predecessor of this node
         */
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    // Used to establish initial head or SHARED marker
        }

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

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

CLH 队列原理图

在这里插入图片描述

CLH 队列是双向链表的实现,只有一个地方比较特殊,就是入队时,第一个入队节点之前会加入一个没有设置节点对应的线程和mode模式的 new Node() 节点,这个主要是因为出队时的逻辑限制,CLH 队列入队和出队主要对应如下几个方法

入队逻辑方法

    // 没有获得锁,当前线程加入队列排队
    // 实际就是 CHL 队列的入队逻辑
    private Node addWaiter(Node mode) {
        // 通过当前线程和排队模式新建 Node 节点
        // Node.EXCLUSIVE 为独占模式; Node.SHARED 为共享模式
        Node node = new Node(Thread.currentThread(), mode);
        // 当前队列的队尾元素
        Node pred = tail;
        // 当前队列的队尾不为 null
        if (pred != null) {
            // 设置当前节点的 prev(前置节点) 为 tail
            node.prev = pred;
            // CAS 设置 pred 为 node(就是将当前 node 设置为 tail)
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 当前队尾为 null 执行 enq 逻辑
        enq(node);
        return node;
    }

    private Node enq(final Node node) {
        for (;;) {
            // t 指向队尾元素
            Node t = tail;
            // 当前队尾为 null
            if (t == null) { // Must initialize
                // 新建一个 Node 设置为 head
                if (compareAndSetHead(new Node()))
                    tail = head;
                // 这里没有 return 会继续循环下一次
            } else {
                // 当前节点的 prev 指向队尾元素
                node.prev = t;
                // 设置 t 为 当前节点
                if (compareAndSetTail(t, node)) {
                    // 设置 t 的 next 为当前节点
                    t.next = node;
                    return t;
                }
            }
        }
    }

出队逻辑方法

    final boolean acquireQueued(final Node node, int arg) {
        // 是否失败,默认为 true
        boolean failed = true;
        try {
            // 是否被打断,默认否
            boolean interrupted = false;
            for (;;) {
                // 获取当前节点的前驱节点(前驱节点为空将抛异常)
                // 这也是为什么 CLH 队列第一个节点为 new Node() 的原因
                // new Node只是一个占位节点
                final Node p = node.predecessor();
                // 如果前驱节点为 head 节点
                // 且 tryAcquire 成功,则前驱节点出队
                if (p == head && tryAcquire(arg)) {
                    // 设置 head 为当前节点
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    // 返回否 false,将不会执行 selfInterrupt()
                    // 即不会执行当前线程的 interrupt 方法
                    return interrupted;
                }
                // shouldParkAfterFailedAcquire 检查 prev 的 waitStatus 状态
                // 为 SIGNAL 才返回 true
                // parkAndCheckInterrupt() park 当前线程(开始阻塞)
                // 当执行的线程 unpark 后,返回当前线程是否被打断
                if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            // 如果出队失败,则取消掉当前节点
            if (failed)
                cancelAcquire(node);
        }
    }

出队逻辑和入队逻辑都是 AQS 类的方法。

继承 AQS 需要实现的几个方法

    /**
     * 尝试以独占模式获取锁
     *
     * 执行获取锁的线程都会调用此方法,如果此方法返回失败,则将尚未排队的线程加入队列排队,直到收到其他线程发出的释放信号。
     * 此方法用于实现 Lock.tryLock() 方法
     *
     * 默认实现是抛出 UnsupportedOperationException 异常
     *
     * @param arg 原则上是可以表示任何内容的值。主要用于判断是否可以获取、以及是否需要等待等
     * @return true:表示可以获取锁,false 表示无法获取锁
     * @throws IllegalMonitorStateException 非法的同步器状态
     * @throws UnsupportedOperationException 如果不支持独占模式
     */
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

    /**
     * 尝试释放独占锁
     *
     * 所有的执行线程都会调用此方法来释放锁
     *
     *
     * 默认实现是抛出 UnsupportedOperationException 异常
     *
     * @param arg 原则上是可以表示任何内容的值。主要用于判断是否可以获取、以及是否需要等待等
     * @return true:表示锁释放成功,其他等待的线程可以尝试获取了,false 表示释放锁失败
     * @throws IllegalMonitorStateException 非法的同步器状态
     * @throws UnsupportedOperationException 如果不支持独占模式
     */
    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }

    /**
     * 尝试以共享模式获取锁
     *
     * 执行获取锁的线程都会调用此方法,如果此方法返回失败,则将尚未排队的线程加入队列排队,直到收到其他线程发出的释放信号。
     *
     * 默认实现抛出:UnsupportedOperationException 异常
     *
     * @param arg 原则上是可以表示任何内容的值。主要用于判断是否可以获取、以及是否需要等待等
     * @return 小于 0,表示在共享模式下获取失败
     * 		   等于 0,表示在共享模式下获取成功,且没有后续的共享模式能够成功获取
     * 		   大于 0,表示在共享模式下获取成功,且后续的共享模式获取也能够成功。在此情况下,后续的等待线程必须检查可用性。
     * @throws IllegalMonitorStateException 非法的同步器状态
     * @throws UnsupportedOperationException 如果不支持共享模式
     */
    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }

    /**
     * 尝试释放共享锁
     *
     * 所以执行的线程都会执行此方法来释放共享锁
     *
     * 默认实现抛出:UnsupportedOperationException 异常
     *
     * @param arg 原则上是可以表示任何内容的值。主要用于判断是否可以获取、以及是否需要等待等
     * @return 成功释放,且其他等待线程可以尝试获取锁,则返回 true,否则返回 false
     * @throws IllegalMonitorStateException 非法的同步器状态
     * @throws UnsupportedOperationException 如果不支持共享模式
     */
    protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }

    /**
     * Returns 如果当前线程持有锁,则返回 true.  
     * ConditionObject 的 signal 方法在调用时都会调用此方法
     *
     * 此方法仅在 AbstractQueuedSynchronizer 内部调用。
     * 用于 ConditionObject ,如果不使用 ConditionObject 可以不用实现 
     *
     * @return 如果当前线程持有锁,则返回 true. 
     * @throws UnsupportedOperationException 如果 ConditionObject 不支持
     */
    protected boolean isHeldExclusively() {
        throw new UnsupportedOperationException();
    }

AQS 对象序列化

AQS 的序列化只存储底层的原子整数维护状态,因此反序列化的对象具有空线程队列。所以我们需要定义一个 readObject 方法,该方法在反序列化时将其恢复到已知的初始状态。

ReentrantLock 源码解析

ReentrantLock 涉及到的源码继承关系图

在这里插入图片描述

这里的红色线都表示为静态内部类,ReentrantLock 有 Sync 和 FairSync、NonFairSync 三个静态内部类,AQS 有 Node 和 ConditionObject 两个静态内部类。

ReentrantLock Lock 加锁过程源码解析

注:这里是使用的 new ReentrantLock() 方式,其实就是 NonfairSync(非公平锁的方式)

请添加图片描述

看不清可以点击大图查看,也可以通过本文最上面的链接下载图片来查看,由于平台限制(最5M)我本地画的高清图片无法上传。

ReentrantLock unlock 解锁过程源码解析

请添加图片描述

关于源码的解析,要写成文章,真是太难了,这里也只是把相关的逻辑做成图片的形式更方便梳理其中的逻辑,图中也将相关的源码方法都贴了上去,并做了相关的逻辑解释,这些解释并不一定准确,希望对大家有所帮助

总结

这里我们只分析了 ReentrantLock 的 lock、unlock 方法的源码分析,ReentrantLock 的其他方法以及 ReentrantReadWriteLock、CountDownLatch、Semaphore 等实现都与我们分析的这些差不太多,我们就不再一一的做解析了,这里我们先总结一下,源码分析的过程与方法,最后再总结一下结论吧。

  1. CLH 队列由双向链表实现
  2. CLH 队列的元素第一个 head 节点为 new Node()
  3. CLH 队列的每个元素包含要执行的线程 thread、锁的共享方式 mode、前驱节点 prev、后继节点 next、状态 waitStatus
  4. AQS 底层实现等待的方法是 LockSupport.park 和 LockSupport.unpark

建议大家最好是自己通过 IDE debug 走一走 lock、unlock 或者 tryLock 等方法的源码,这样才能更有印象。

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

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

相关文章

Day7 vitest 之 vitest配置第三版

项目目录 runner Type: VitestRunnerConstructor Default: node, 当运行test的时候 benchmark,当运行bench测试的时候 功能 自定义测试运行程序的路径。 要求 应与自定义库运行程序一起使用。 如果您只是运行测试,则可能不需要这个。它主要由library作者使用 …

kubernetes(K8s)的使用和常用命令

K8S kubernetes,由于k和s之间有8个字符,所以简称k8s,是一个全新的基于容器技术的分布式架构领先方案,是谷歌严格保密十几年的秘密武器----Borg系统的一个开源版本,于2015年7月发布第一个正式版本,它的本质…

HTML如何设置多图片上传,并限制格式类型

在HTML如何设置多图片上传&#xff0c;并限制格式类型为jpg和png格式。 <input type"file" name"fileInput" id"fileInput"> 上面这行代码&#xff0c;只支持单个文件上传&#xff0c;且不支持文件类型过滤&#xff0c;在实际开发过程中&…

HTML5+CSS3+Vue小实例:彩色圆环溶解加载动画

实例:彩色圆环溶解加载动画 技术栈:HTML+CSS+Vue.js 效果: 源码: 【HTML】 <!DOCTYPE html> <html lang="zh-CN"> <head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge&…

「HarmonyOS」验证码多TextInput输入框焦点自动跳转问题

需求背景&#xff1a;需要做一个多输入框的验证码模块&#xff0c;输入验证码时输入后光标会自动跳转至下一个输入框&#xff0c;删除验证码时会自动删除上一个输入框内容&#xff0c;并且光标跳转至上一个输入框内。6位验证码全部输完后进行登录请求 具体样式如下图&#xff1…

RFID数据中心智能资产管理系统

数据中心机房承担着保障企业关键数据处理的重要责任&#xff0c;机房的日常管理直接关系到整体机房的日常维护和运行安全&#xff0c;数据资产管理中心在监管机房各部分设备的运行情况、维护数据中心的资产方面发挥着重要的作用。 成功的数据中心机房管理不仅需要选择高可靠性…

理解UML中的依赖关系

理解UML中的依赖关系 在面向对象的设计中&#xff0c;理解各种类之间的关系对于构建一个清晰、可维护的系统至关重要。UML&#xff08;统一建模语言&#xff09;为我们提供了一种可视化这些关系的方式。今天&#xff0c;我们将深入探讨UML中的依赖关系&#xff08;Dependency&a…

Kubernetes 核心实战之一(精华篇 1/2)

文章目录 1&#xff0c;资源创建方式1.1 yaml1.2 命令行 2&#xff0c;NameSpace命名空间2.1 命令行创建ns2.2 yaml 创建ns 3&#xff0c;Pod3.1 命令行 创建pod3.2 yaml 创建pod3.3 可视化界面 创建3.3.1 Pod nginx3.3.2 Pod nginx tomcat3.3.3 Pod 2ngnix 1&#xff0c;资源…

ThreadLocal线程重用导致用户信息错乱的 Bug

在生产上遇到一个诡异的问题&#xff0c;有时获取到的用户信息是别人的。查看代码后&#xff0c;我发现他使用了 ThreadLocal 来缓存获取到的用户信息。 我们知道&#xff0c;ThreadLocal 适用于变量在线程间隔离&#xff0c;而在方法或类间共享的场景。如果用户信息的获取比较…

软件测试第二部分:白盒测试

概念与定义 白盒测试&#xff1a;侧重于系统或部件内部机制的测试&#xff0c;类型分为分支测试&#xff08;判定节点测试&#xff09;、路径测试、语句测试。 控制流分析(基于程序结构)&#xff1a;控制流分析是一类用于分析程序控制流结构的静态分析技术&#xff0c;目的在于…

8个流行的Python可视化工具包,你喜欢哪个?

用 Python 创建图形的方法有很多&#xff0c;但是哪种方法是最好的呢&#xff1f;当我们做可视化之前&#xff0c;要先明确一些关于图像目标的问题&#xff1a;你是想初步了解数据的分布情况&#xff1f;想展示时给人们留下深刻印象&#xff1f;也许你想给某人展示一个内在的形…

芯课堂 | LVGL基础知识(二)

引言 在 LVGL 中&#xff0c;用户界面的基本构建块是对象&#xff0c;也称为小部件(widget)。默认情况下&#xff0c;LVGL在背景上绘制旧对象&#xff0c;在前景上绘制新对象。 对象层级(Layers) 创建对象层级顺序 默认情况下&#xff0c;LVGL在背景上绘制旧对象&#xff0c…

【导出与导入Virtualbox虚拟机和启动连接openGauss数据库】

【导出与导入Virtualbox虚拟机和启动连接openGauss数据库】 一、导出虚拟机二、导入虚拟机三、启动数据库四、使用Data Studio连接数据库 一、导出虚拟机 选择关机状态的虚拟机 -> 管理菜单 -> 导出虚拟电脑 点击完成后&#xff0c;需要等待一小段时间&#xff0c;如…

基于gitlab 12.8.0版本的完整镜像过程

目前已在一台服务器上安装了gitlab 12.8.0&#xff0c;并且稳定运行了有几年了&#xff0c;其上面也创建了大量的项目。目前要求对该gitlab及其上面的所有仓库做一个完整的镜像。具体操作过程如下&#xff1a; 1、确认现有的gitlab的版本号 2、到gitlab官网下载相同版本号的gi…

【干货】Windows中定时删除system32目录下的.dmp文件教程

旭帆科技的技术人员除了给用户答疑解惑以外&#xff0c;还会主动测试软件性能&#xff0c;进行平台优化&#xff0c;除此之外&#xff0c;技术人员还会总结一些技术干货&#xff0c;这不&#xff0c;近期又提供了一份如何在Windows中定时删除system32目录下的.dmp文件的教程。感…

目标检测COCO数据集与评价体系mAP

1.mAP 2.IoU IoU也就是交并比&#xff0c;也称为 Jaccard 指数&#xff0c;用于计算真实边界框与预测边界框之间的重叠程度。它是真值框与预测边界框的交集和并集之间的比值。Ground Truth边界框是测试集中手工标记的边界框&#xff0c;用于指定对象图像的位置以及预测的边界框…

软件安全测试有哪些测试方法?安全测试报告如何申请?

随着网络条件越来越好&#xff0c;软件产品质量和软件安全处于同等重要的地位&#xff0c;一款好的软件产品安全系数至关重要&#xff0c;因此安全测试必不可少&#xff0c;那么软件安全测试有哪些测试方法呢?安全测试报告又该如何申请&#xff1f; 软件安全测试是指测试人员…

BUUCTF--ciscn_2019_n_31

这是一题32位的堆题&#xff0c;照常看看保护&#xff1a; 没有开启PIE&#xff0c;接着进行黑盒测试&#xff1a; 菜单题&#xff0c;扔进IDA看看代码逻辑&#xff1a; 4这个功能是提供所谓的进阶版&#xff0c;当时我测试的时候以为是里面有后门还是什么的。结果发现是虚晃一…

(17)Linux的进程阻塞进程程序替换 exec 函数簇

前言&#xff1a;本章我们讲解它的 options 参数。在讲解之前我们需要理解进程阻塞&#xff0c;然后我们重点讲解二进程程序替换&#xff0c;这是本章的重点&#xff0c;然后介绍一个进程替换函数 execl&#xff0c;通过介绍这个函数来打开突破口&#xff0c;引入进程创建的知识…

dubbo使用的三种配置

一. 准备注册中心 dubbo的注册中心在生产环境中&#xff0c;一般都会选择 ZooKeeper 下载 ZooKeeper ZooKeeper_3.4.14下载地址启动ZK # 解压安装包 tar -zxvf zookeeper-3.4.14.tar.gz# 进入安装目录&#xff0c; cp conf/zoo_sample.cfg conf/zoo.cfg# 启动ZK ./bin/zkServ…