AQS原来是这么设计的,泰裤辣!

news2025/1/19 8:27:03

缘起

每门编程语言基本都离不开并发问题,Java亦如此。谈到Java的并发就离不开Doug lea老爷子贡献的juc包,而AQS又是juc里面的佼佼者
因此今天就一起来聊聊AQS

概念

AQS是什么,这里借用官方的话
Provides a framework for implementing blocking locks and related synchronizers that rely on first-in-first-out wait queues

AQS的全程是AbstractQueuedSynchronizer,在这里咱们进行咬文嚼字一下。
Abstract:这是AQS采用模板设计模式的基础,AQS中将定义了大部分同步的流程,仅将加解锁的操作留给子类根据需求进行自定义(这也就是为什么使用AQS可以快速开发锁或者同步器的主要原因)
Queued:这里指的是CLH队列;当共享资源被占用时,就需要一套线程阻塞等待以及被唤醒时锁分配机制,AQS通过CLH队列来实现
CLH是一个虚拟的双向链表实现的队列,获取不到锁资源时会被AQS封装成Node节点并加入队列。每个线程执行结束都会唤醒其下一个节点
Synchronizer:同步控制,AQS的同步控制实现有两种。一种是volatile+CAS的乐观锁设计,另一种是LockSupport+CAS的悲观锁设计

切入点

  1. 模板方法设计
  2. 可重入设计
  3. CLH队列设计
  4. 公平/非公平锁设计

模板方法设计

我们来看看AQS的设计流程

从图中可看到,AQS设计并且实现了一个同步器/锁的完整流程;
但是将tryAcquire/tryAcquireShared和tryRelease/tryReleaseShared这些经常改动的操作设置为抽象方法,留给子类自行拓展

acquire方法剖析
这个方法采用门面设计模式,将CAS获取锁,封装线程以及添加CLH队列的操作封装成一个方法并对外提供
我们常用的ReentrantLock.lock方法实际上就是调用此方法

public final void acquire(int arg) {
    // 尝试通过CAS获取锁,若获取成功则执行同步代码块
    // 获取失败则通过addWaiter将当前线程封装成AQS的Node节点并加入CLH队列,同时中断当前线程
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

addWaiter方法剖析
Node是AQS的内部类,Node是组成CLH队列的节点
申请公平/非公平锁失败都会被加入CLH队列

private Node addWaiter(Node mode) {
    // 1. 将当前线程跟Node关联起来,方便AQS根据队列顺序唤醒获取锁
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    // 2. 追加新建节点到双向链表尾
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 3. 初始化链表
    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;
                return t;
            }
        }
    }
}

acquireQueued方法剖析

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 1. 获得当前Node的前驱节点,也就是当前任务的前一个任务
            final Node p = node.predecessor();
            // 2. 如果前一个任务是head则尝试通过CAS来获取锁
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //3. 通过UNSAGE.park来阻塞当前线程来等待许可
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

小结
通过这小节我们可以看到AQS是如何通过模板方法设计模式大大简化了同步器/锁开发的

可重入设计

ReentrantLock是可重入锁的典型设计,在这里就基于它进行分析

ReentrantLock的锁是组合设计,通过内部类Sync、NonfairSync和FairSync来实现
这里看看非公平锁NonfairSync的实现

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //1. 如果当前锁空闲,则获取锁并设置锁的Owner为当前线程
    if (c == 0) {      
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 2. 如果当前锁已经被获取,则判断锁的Owner是否是当前线程
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}


protected final boolean tryRelease(int releases) {
    // 1. 进行锁释放时对state进行减法,由于加锁和释放锁的操作都是配对出现的。那么重入2次时状态为3,那么释放锁的时候也会释放3次直到状态state为0时才释放锁
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        // 2. 释放锁的时候先将锁的Owner置空
        setExclusiveOwnerThread(null);
    }
    // 3. 释放锁
    setState(c);
    return free;
}

锁Owner实现
AQS的锁Owner是通过继承父类AbstractOwnableSynchronizer来实现的
AbstractOwnableSynchronizer只有一个成员属性exclusiveOwnerThread,用来标识独占锁场景下是哪个线程持有锁,方便可重入判断

public abstract class AbstractOwnableSynchronizer
    implements java.io.Serializable {
  
    private transient Thread exclusiveOwnerThread;

    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
}

小结
通过分析我们可以看到,AQS的可重入设计是通过父类AbstractOwnableSynchronizer+volatile修饰的state作为计数器来实现的

CLH队列设计

当共享资源被占用时,就需要一套线程阻塞等待以及被唤醒时锁分配机制,AQS通过CLH队列来实现
CLH队列保证了锁可以高效进行分配,避免了无意义的唤醒阻塞未获得锁线程

CLH的入口在于AQS的acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法,它是一个虚拟的双向链表,addWaiter会将请求锁的线程封装成Node节点,Node节点中通过存储前后序节点来维护双向队列

源码剖析

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 1. 获取前序节点
            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);
    }
}

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
      int ws = pred.waitStatus;
      if (ws == Node.SIGNAL)
          return true;
      // 将当前节点所有直接前驱节点从CLH队列中移除
      if (ws > 0) {
          do {
              node.prev = pred = pred.prev;
          } while (pred.waitStatus > 0);
          pred.next = node;
      } else {
          compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
      }
      return false;
}

private final boolean parkAndCheckInterrupt() {
    //请求锁的线程加入CLH队列中,通过调用LockSupport、UNSAFE来进行线程的阻塞,直至CLH链表中上一个节点释放锁后才会主动唤醒
    LockSupport.park(this);
    return Thread.interrupted();
}

LockSupport源码实现

public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    // 这是做什么用的?
    setBlocker(t, blocker);
    // 最终调用UNSAFE来执行
    UNSAFE.park(false, 0L);
    setBlocker(t, null);
}

// 在UNSAFE中显示这是个本地方法,是由C++来实现的
public native void park(boolean var1, long var2);

// C++通过给当前线程添加一个互斥量0,每次该线程获得CPU时间片都会判断该互斥量,为0则将当前线程切换为阻塞状态
// 直到其他线程通过unpark来将此互斥量修改为1,该方法才会继续往下执行

小结
CLH队列给AQS维护了一个高效率的锁分配/释放的基础

公平/非公平锁设计

JDK中公平锁和非公平锁的具体实现分别是FairSync和NonfairSync
FairSync的获取锁流程

NonfairSync的获取锁流程

小结
通过分析可看到公平锁和非公平锁的获取锁/释放锁逻辑几乎一致,这都要归功于AQS的模板方法设计模式
两者不同的地方在于获取公平锁的请求会直接加入到CLH队列中等待锁,获取非公平锁的请求都会尝试直接获取锁,获取失败再加入CLH队列进行等待
非公平锁的性能相对而言更好,一般也是首选,但是会存在锁饥饿的现象;公平锁可以有效解决锁饥饿的问题,但性能相对而言会差一些

总结

  1. 在AQS中可以看到不少优秀的设计,这都要归功于Doug Lea老爷子;除了AQS,在juc里还有很多优秀的设计,如并发性能最好的字典ConcurrentHashMap、无锁高性能队列ConcurrentLinkedQueue等等
    在品完源码后,你会发现其设计思想丝毫不逊色于各个大数据组件
  2. “曾经想征服全世界,到最后回头才发现,这世界点点滴滴全部都是你” ——致JDK

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

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

相关文章

【Linux】常用的基本命令指令②

前言&#xff1a;前面我们学习了Linux的部分指令&#xff0c;今天我们将接着上次的部分继续将Linux剩余的基本指令. &#x1f496; 博主CSDN主页:卫卫卫的个人主页 &#x1f49e; &#x1f449; 专栏分类:Linux的学习 &#x1f448; &#x1f4af;代码仓库:卫卫周大胖的学习日记…

了解Service Mesh:构建微服务的下一代架构

1. 引言 服务网格&#xff08;Service Mesh&#xff09;作为构建下一代微服务架构的关键技术&#xff0c;能够显著地简化和改善微服务架构的管理和运维&#xff0c;并提供了更高的可靠性、安全性和可观察性。今天就和大家一起了解下什么是Service Mesh&#xff0c;它能够做什么…

bootstrap5实现的高端蔬菜食品网页Obrien

一、需求分析 蔬菜超市在线系统是指一个基于互联网的平台&#xff0c;使消费者可以通过网络购买蔬菜和其他相关农产品。它提供了一种便捷的购物方式&#xff0c;消费者可以通过在线浏览商品、选择购买、支付和配送等步骤来完成购物过程。以下是蔬菜超市在线系统的一些功能&…

K-最近邻算法(KNN)是什么算法?

K-最近邻算法&#xff08;K-Nearest Neighbor&#xff0c;KNN&#xff09;是一种经典的有监督学习方法&#xff0c;也可以被归为懒惰学习&#xff08;Lazy Learning&#xff09;方法。它基于“物以类聚”的原理&#xff0c;假设样本之间的类别距离越近则它们越有可能是同一类别…

MT9284-28J 高效率升压白光LED驱动器IC SOT23-6 航天民芯

描述 MT9284是一个升压转换器&#xff0c;设计用于从单电池锂离子电池中驱动多达7个系列的白色led。MT9284使用电流模式&#xff0c;固定频率结构来调节LED电流&#xff0c;它通过外部电流感应电阻来测量。MT9284包括低电压锁定、限流和热过载保护&#xff0c;以防止在输出过载…

【MIT 6.S081】2020, 实验记录(1),Lab: Xv6 and Unix utilities

目录 实验准备TasksTask 1: Boot xv6Task 2: sleepTask 3: pingpongTask 4: primesTask 5: find 实验准备 这个 lab 用来学习尝试如何通过 system call 来实现常见的 shell 命令行程序&#xff0c;比如 ls、sleep、xargs 等。 实验官网 可以使用 docker 搭建实验环境&#x…

全志R128 SDK架构与目录结构

R128 S2 是全志提供的一款 M33(ARM)C906(RISCV-64)HIFI5(Xtensa) 三核异构 SoC&#xff0c;同时芯片内部 SIP 有 1M SRAM、8M LSPSRAM、8M HSPSRAM 以及 16M NORFLASH。本文档作为 R128 FreeRTOS SDK 开发指南&#xff0c;旨在帮助软件开发工程师、技术支持工程师快速上手&…

MathType2024MAC苹果电脑版本下载安装图文教程

在数学和科学的世界里&#xff0c;表达精确的方程式和化学公式是至关重要的。MathType作为一款及其优秀且有全球影响力的数学公式编辑器&#xff0c;让这一切变得触手可及。MathType Mac版已全新升级&#xff0c;作为Microsoft Word和PowerPoint的Add-In插件&#xff0c;为您的…

项目使用PowerJob

新一代的定时任务框架——PowerJob 简介 PowerJob是基于java开发的企业级的分布式任务调度平台&#xff0c;与xxl-job一样&#xff0c;基于web页面实现任务调度配置与记录&#xff0c;使用简单&#xff0c;上手快速&#xff0c;其主要功能特性如下&#xff1a; 使用简单&…

CEC2017(Python):七种算法(PSO、RFO、DBO、HHO、SSA、DE、GWO)求解CEC2017

一、7种算法简介 1、粒子群优化算法PSO 2、红狐优化算法RFO 3、蜣螂优化算法DBO 4、哈里斯鹰优化算法HHO 5、麻雀搜索算法SSA 6、差分进化算法DE 7、灰狼优化算法GWO 二、CEC2017简介 参考文献&#xff1a; [1]Awad, N. H., Ali, M. Z., Liang, J. J., Qu, B. Y., &am…

华为云Stack 8.X流量模型分析(六)

八、基础云专线流量模型分析 ​ 华为官方对云专线定义是&#xff1a;用户数据中心通过运营商的物理专线&#xff08;MPLS/VPN&#xff0c;以太专线&#xff09;访问云内资源&#xff0c;云内资源呈现真实IP&#xff0c;通过设置的专线路由实现三层互通。 ​ 通过云专线直接访…

LeetCode(36)有效的数独 ⭐⭐

请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 &#xff0c;验证已经填入的数字是否有效即可。 数字 1-9 在每一行只能出现一次。数字 1-9 在每一列只能出现一次。数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。&#xff08;请参考示例图&#xff09; 注…

ssm基于web的网络游戏交易平台信息管理系统的设计与实现论文

摘 要 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff0c;在计算机上安装网络游戏交易平台软件来发挥其高效地信息处理的作用&#x…

SecOC中新鲜度值和MAC都按照完整的值来生成,但是在发送和认证的时候只会截取一部分。这边截取的部分一般取多长?由什么参数设定?

新鲜度值(Freshness Value, FV)和消息验证码(Message Authentication Code, MAC)是SecOC协议中用于保证数据的真实性和新鲜度的重要信息。它们的长度取决于不同的因素,如加密算法、安全级别、通信带宽等。 一般来说,FV和MAC的长度越长,安全性越高,但也会占用更多的通信…

Kibana 自定义索引连接器告警

一、 创建索引 PUT ipu-cbs-warning-info{"settings" : {"number_of_shards" : 1},"mappings" : {"properties" : {"timestamp": {"type": "date"},"rule_id" : { "type" : "…

面试官:CSS3新增了哪些新特性?

面试官&#xff1a;CSS3新增了哪些新特性&#xff1f; 一、是什么 css&#xff0c;即层叠样式表&#xff08;Cascading Style Sheets&#xff09;的简称&#xff0c;是一种标记语言&#xff0c;由浏览器解释执行用来使页面变得更美观 css3是css的最新标准&#xff0c;是向后兼…

十、基本对话框大集合(Qt5 GUI系列)

目录 一、设计需求 二、实现代码 三、代码解析 四、总结 一、设计需求 Qt提供了很多标准的对话框。例如标准文件对话框(QFileDialog)、标准颜色对话框(QColorDialog)、标准字体对话框 (QFontDialog)、标准输入对话框 (QInputDialog) 及消息对话框 (QMessageBox)。本文展示各…

编写.NET的Dockerfile文件构建镜像

创建一个WebApi项目&#xff0c;并且创建一个Dockerfile空文件&#xff0c;添加以下代码&#xff0c;7.0代表的你项目使用的SDK的版本&#xff0c;构建的时候也需要选择好指定的镜像tag FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443F…

[论文笔记] Megtron_LM 0、报错:vscode调试无法传进去参数 launch.json文件获取args参数

解决方法&#xff1a; 配置好launch.json文件后&#xff0c;应该点运行和调试里面的运行按钮。而不是直接点文件右上角的debug。 可以看到terminal中&#xff0c;如果没有正常加载launch.json&#xff0c;则参数中没有args的参数。 如果正常加载&#xff0c;可以看到args的很多…

docker镜像仓库详解(Docker Registry)

本片文章主要是对docker的镜像仓库进行了详解。其中包含了一些常用了 docker 指令&#xff0c;通过举例进行详解。也详细解释了镜像仓库的工作机制和常见的镜像仓库。也实际拉去和运行了一些镜像。希望本篇文章会对你有所帮助&#xff01; 文章目录 一、什么是Docker Registry …