队列同步器AQS的实现与分析——独占锁模式

news2024/11/25 21:36:40

AQS独占锁模式源码分析

  • 1、tryAcquire()、acquire()方法
  • 2、addWaiter()方法
  • 3、acquireQueued()方法
  • 4、shouldParkAfterFailedAcquire()方法
  • 5、tryRelease()、release()方法

1、tryAcquire()、acquire()方法

protected boolean tryAcquire(int arg) {
     throw new UnsupportedOperationException();
}

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

若上层调用tryAcquire返回true,线程获得锁,此时可以对相应的共享资源进行操作,使用完之后再进行释放。如果调用tryAcquire返回false,且上层逻辑上不想等待锁,那么可以自己进行相应的处理(@override tryAcquire()方法);如果上层逻辑选择等待锁,那么可以直接调用acquire方法,acquire方法内部封装了复杂的排队处理逻辑,非常易用。

假如tryAcquire返回false,说明需要排队,那么就进而执行
acquireQueued(addwaiter(Node.EXCLUSIVE),arg),acquireQueued方法其中嵌套了addWaiter方法。

2、addWaiter()方法

将当前线程封装成Node加入等待队列的队尾。

private Node addWaiter(Node mode) {
     Node node = new Node(Thread.currentThread(), mode);
     // Try the fast path of enq; backup to full enq on failure
     // 先尝试用最快的方式入队,如果失败再执行完整的入队方法
     Node pred = tail;
     if (pred != null) {
         node.prev = pred;
         if (compareAndSetTail(pred, node)) {
             pred.next = node;
             return node;
         }
     }
     enq(node);
     return node;
}

上述代码通过使用compareAndSetTail(Node expect,Node update)方法来确保节点能够被线程安全添加。

完整的入队方法 enq()

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;
             }
         }
     }
}

在enq(final Node node)方法中,同步器通过 “死循环” 来保证节点的正确添加,在 “死循环” 中只有通过CAS将节点设置成尾节点之后,当前线程才能从该方法返回,否则,当前线程不断地尝试设置。可以看出,enq(final Node node)方法将并发添加节点的请求通过CAS变得 “串行化” 了。

节点进入同步队列之后,就进入了一个自旋的过程,每个节点(或者说每个线程)都在自省地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中(并会阻塞节点的线程)

AQS在各个线程中维护了当前Node的waitStatus,根据不同的状态,程序来做出不同的操作。通过调用acquireQueued 方法,开始对Node的waitStatus进行跟踪维护。

3、acquireQueued()方法

final boolean acquireQueued(final Node node, int arg) {
     boolean failed = true;
     try {
         boolean interrupted = false;
         for (;;) {
             final Node p = node.predecessor();  //获取当前节点的前置节点
             
             if (p == head && tryAcquire(arg)) {  //如果当前节点的前置节点为head,且当前节点成功地获得锁
                 setHead(node);
                 p.next = null; // help GC
                 failed = false;
                 return interrupted;
             }
             if (shouldParkAfterFailedAcquire(p, node) &&
                 parkAndCheckInterrupt())
                 interrupted = true;
         }
     } finally {
         if (failed)
             cancelAcquire(node);
     }
}

基本可以将acquireQueued分成三部分:
▲第一个 if 判断条件:如果当前节点的前置节点为head,说明当前节点有权限去尝试拿锁。如果tryAcquire返回true,代表拿到了锁,那么顺理成章,函数返回。
▲第二个 if 判断条件:包含两个方法,看名字是首先判断当前线程是否需要挂起等待?如果需要,那么就挂起。并且判断外部是否调用线程中断;如果不需要,那么继续尝试拿锁(自旋)。
▲如果 try 块中抛出非预期异常,那么取消当前线程获取锁的行为。

那么接下来分别来看 shouldParkAfterFailedAcquire()和parkAndCheckInterrupt()方法。

4、shouldParkAfterFailedAcquire()方法

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;
}

若当前节点没有拿锁的权限或拿锁失败,那么将会进入shouldParkAfterFailedAcquire(Node pred, Node node)判断是否需要挂起,方法的参数是pred Node和当前Node的引用。

方法的流程其实注释已经写的很清楚了:

1、若pred的waitSatus为SIGNAL,说明前置节点也在等待拿锁,并且之后将会唤醒当前节点,所以当前线程可以挂起休息,返回true。
2、如果ws大于0,说明pred的waitSatus是CANCEL,所以可以将其从队列中删除。这里通过从后向前搜索,将pred指向搜索过程中第一个waitSatus为非CANCEL的节点。相当于链式地删除被CANCEL的节点。然后返回false,代表当前节点不需要挂起,因为pred指向了新的Node,需要重试外层的逻辑。
3、除此之外,pred的ws还有两种可能,0或PROPAGATE,有人可能会问,为什么不可能是CONDITION?因为waitStatus只有在其他条件模式下,才会被修改为CONDITION,这里不会出现,并且只有在共享模式下,才可能出现waitStatus为PROPAGATE,暂时也不用管。那么在独占模式下,ws在这里只会出现0的情况。0代表pred处于初始化默认状态,所以通过CAS将当前pred的waitStatus修改为SIGNAL,然后返回false,重试外层逻辑。

如果shouldParkAfterFailedAcquire返回false,那么再进行一轮重试;如果返回true,代表当前节点需要被挂起,则执行parkAndCheckInterrupt方法。

private final boolean parkAndCheckInterrupt() {
     LockSupport.park(this);   //线程将在这里被挂起(阻塞)
     return Thread.interrupted();
}

当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具类来完成相应的工作。这里LockSupport.park(this)本质是通过Unsafe下的native方法调用操作系统原语来将当前线程挂起。此时当前Node中的线程将阻塞在此处,如果调用unpark(Thread thread)方法或者当前线程被中断,才能从park()方法返回。

通过对acquireQueued这个方法的分析,我们可以这么说,如果当前线程所在的节点处于头节点的后一个,那么它将会不断去尝试拿锁,直到获取成功。否则进行判断,是否需要挂起。这样就能保证head之后的一个节点在自旋CAS获取锁,其他线程都已经被挂起或正在被挂起。这样就能最大限度地避免无用的自旋消耗CPU。
在这里插入图片描述
当前线程获取同步状态并执行了相应逻辑之后,就需要释放同步状态,使得后续节点能够继续获取同步状态。通过调用同步器的release(int arg)方法可以释放同步状态,该方法在释放了同步状态之后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态)

5、tryRelease()、release()方法

protected boolean tryRelease(int arg) { 
     throw new UnsupportedOperationException();
}

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

该方法执行时,会唤醒头结点的后继节点线程,unparkSuccessor(Node node)方法使用LockSupport来唤醒处于等待状态的线程。

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)
         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);
}

获取head的waitStatus,如果不为0,那么将其置为0,表示锁已释放。接下来获取后续节点如果后续节点为null或者处于CANCELED状态,那么从后往前搜索,找到除了head外最靠前且非CANCELED状态的Node,对其进行唤醒,让它起来尝试拿锁。

被挂起的线程一旦被唤醒,那么它将会继续执行acquireQueued()这个方法进行自旋尝试获取锁。这样就形成了一个良好的闭环,拿锁、挂起、释放、唤醒都能够有条不紊的进行。

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

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

相关文章

glassfish任意文件读取漏洞

glassfish任意文件读取漏洞1.简介1.1.漏洞类型1.2.漏洞成因1.3.语法搜索1.4.影响版本2.漏洞复现2.1.POC2.2.访问地址2.3.GlassFish的敏感目录2.3.1.获取数据库密码2.3.2.获取GlassFish的后台密码2.4.POC脚本1.简介 GlassFish是一款强健的商业兼容应用服务器&#xff0c;达到产品…

MyEclipse提示过期,MyEclipse Subscription Expired激活方案

一、错误描述 紧接上文&#xff0c;虽然解决了MyEclipse提示过期问题&#xff0c;但是你会发现出现一行红色提示如下&#xff1a; 1.错误日志 Product activation must be completed within 5 days. 2.错误说明 产品激活必须在5天内完成。 二、解决方案 从错误日志很明显的可…

C++语法复习笔记-2. c++基础句法

文章目录1. 图灵机与三种基本结构1. 顺序结构2. 分支结构自定义结构-枚举结构体与联合体结构体数据对齐问题3. 循环结构三种循环结构反汇编查看三种结构效率实例&#xff1a;输出所有形如aabb的四位数的完全平方数方案1: 构造aabb数&#xff0c;再判断方案2&#xff1a;反向操作…

《网络编程实战》学习笔记 Day10

系列文章目录 这是本周期内系列打卡文章的所有文章的目录 《Go 并发数据结构和算法实践》学习笔记 Day 1《Go 并发数据结构和算法实践》学习笔记 Day 2《说透芯片》学习笔记 Day 3《深入浅出计算机组成原理》学习笔记 Day 4《编程高手必学的内存知识》学习笔记 Day 5NUMA内存知…

自制DAPLink 基于ARM官方源码以及STM32F103C8T6

【本文发布于https://blog.csdn.net/Stack_/article/details/128771308&#xff0c;未经许可禁止转载&#xff0c;转载须注明出处】 一、安装工具并配置环境变量 1、python3 【官网】 【网盘】 链接&#xff1a;https://pan.baidu.com/s/1zW_H_eQlkzX3FkXuClFnTA 提取码&#…

python 操作 json 文件的种种知识点

本篇博客将带你全方位了解 Python 操作 json 文件的技术点 让你碰到 json 格式文件不在发愁 文章目录json 模块读取 JSON写入 JSON读取与写入基本用法如下json 模块进阶用法控制输出格式在 JSON 中存储 Python 特殊类型对数据进行验证和清洗第三方模块json 模块 Python 提供了…

CE自动汇编之AOB注入

目录 一、什么是AOB注入&#xff1f; 二、什么时候使用AOB注入&#xff1f; 三、代码注入 四、全部注入 五、“全部注入”和“AOB注入”的分别 六、代码注入与AOB注入的区别 CE自动汇编的模板中&#xff0c;有三种注入代码的方式&#xff1a; 第一种是代码注入&#xff…

Qt使用数据库模型中的删除详解

以下使用 QSqlTableModel 模型&#xff0c;使用tableView显示内容 以下为界面&#xff1a; 这里主要介绍删除操作&#xff1a; 删除一行为&#xff1a; int rowui->tableView->currentIndex().row();//获取行号model->revertRow(row);//删除该行model->submitAll(…

git 关于分支和仓库的理解

何时需要initgit init//初始化本地仓库.git目录如果初始化就会在当前文件夹中出现.git的目录&#xff0c;该目录默认是隐藏的&#xff0c;需要关闭显示隐藏文件才能看到。执行完git init命令后&#xff0c;当前目录就成为了工作区&#xff08;工作区可以理解为操作本地仓库的车…

MyBatis-Plus知识快速入门

文章目录1.MyBatis-Plus简介2.入门案例2.1开发环境2.2创建测试数据库和表2.3创建SpringBoot工程2.4创建实体类以及lombok的使用2.5添加mapper2.6加入日志功能3.基本的CRUD3.1BaseMapper3.2插入3.3删除3.4修改3.5查询4.通用Service4.1创建Service接口和实现类5.常用注解5.1Table…

“华为杯”研究生数学建模竞赛2005年-【华为杯】A题:城市出租车交通规划综合模型(附获奖论文和matlab代码)

赛题描述 A: Highway Traveling time Estimate and Optimal Routing Ⅰ Highway traveling time estimate is crucial to travelers. Hence, detectors are mounted on some of the US highways. For instance, detectors are mounted on every two-way six-lane highways o…

springboot 分布式全局唯一id的生成-雪花算法snowflake

一 背景描述 1.1 问题产生 在分布式系统中&#xff0c;怎么使用全局唯一id&#xff1f; 在分布式是&#xff0c;微服务的架构中&#xff0c;或者大数据分库分表中&#xff0c;多个不同节点怎么保持每台机器生成的主键id不重复&#xff0c;具有唯一性&#xff1f; 方案1&…

【算法基础】归并排序(原理、过程、例题、代码)

一、归并排序原理 1. 算法介绍 归并排序是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有…

力扣 2309. 兼具大小写的最好英文字母

题目 给你一个由英文字母组成的字符串 s &#xff0c;请你找出并返回 s 中的 最好 英文字母。返回的字母必须为大写形式。如果不存在满足条件的字母&#xff0c;则返回一个空字符串。 最好 英文字母的大写和小写形式必须 都 在 s 中出现。 英文字母 b 比另一个英文字母 a 更…

前端食堂技术周刊第 68 期:Astro 2.0、Nuxt v3.1.0、Bun v0.5、TS 实现 Stage 3 Decorators 提案

美味值&#xff1a;&#x1f31f;&#x1f31f;&#x1f31f;&#x1f31f;&#x1f31f; 口味&#xff1a;萝卜牛腩煲 食堂技术周刊仓库地址&#xff1a;https://github.com/Geekhyt/weekly 本期摘要 Astro 2.0Nuxt v3.1.0Bun v0.5TS 实现 Stage 3 Decorators 提案Turbore…

【算法突击】排序算法系列(一) | 程序员面试 | 冒泡排序 | 快速排序 | 归并排序

【排序算法】 冒泡排序 | 快速排序 | 归并排序 文章目录【排序算法】 冒泡排序 | 快速排序 | 归并排序1. 冒泡排序1.1 核心思想1.2 代码实现2. 快速排序2.1 核心思想2.2 时间复杂度2.2 代码实现3. 归并排序3.1 核心思想3.2 时间复杂度3.3 代码实现1. 冒泡排序 1.1 核心思想 将…

【接口自动化】接口间参数传递的一种解决方案

本文转载自&#xff1a;接口间参数传递的一种解决方案 做过接口自动化测试的同学肯定都熟悉在全链路测试过程中&#xff0c;很多业务场景的完成并非由单一接口实现&#xff0c;而是由很多接口组成的一条链路实现。例如你在淘宝上购物场景。 不同于单接口测试&#xff0c;这种链…

2 线性模型

文章目录一般流程问题引入数据集与测试集过拟合与泛化开发集监督学习和非监督学习问题分析训练集、验证集、测试集模型设计模拟训练过程课程代码课后习题代码课程来源&#xff1a; 链接文档参考&#xff1a; 链接以及 BirandaのBlog&#xff01;一般流程 对于一般的线性模型来…

微信小程序 Springboot校园招聘求职系统

基于微信小程序的校园求职系统的设计基于现有的手机&#xff0c;可以实现首页、个人中心、岗位类型管理、用户管理、企业管理、招聘信息管理、应聘信息管理、系统管理等功能。方便用户对首页、招聘信息、我的等详细的了解及统计分析 一个基本的程序包含app.json、project.confi…

谈谈SpringBoot(二)

1. Spring Boot缓存 1.1 JSR-107 Spring从3.1开始定义了org.springframework.cache.Cache 和org.springframework.cache.CacheManager接口来统一不同的缓存技术&#xff1b; 并支持使用JCache&#xff08;JSR-107&#xff09;注解简化我们开发。 Cache接口为缓存的组件规范定义…