Java学习笔记(多线程):ReentrantLock 源码分析

本文是自己的学习笔记,主要参考资料如下
JavaSE文档


  • 1、AQS 概述
    • 1.1、锁的原理
    • 1.2、任务队列
      • 1.2.1、结点的状态变化
    • 1.3、加锁和解锁的简单流程
  • 2、ReentrantLock
    • 2.1、加锁源码分析
      • 2.1.1、tryAcquire()的具体实现
      • 2.1.2、acquirQueued()的具体实现
      • 2.1.3、tryLock的具体实现
      • 2.1.5、总结

1、AQS 概述

1.1、锁的原理

AQS是指抽象类AbstractQueuedSynchronizer。这个抽象类代表着一种实现并发的方式。

具体实现方式是使用volitile修饰state变量,保证了state的可见性和有序性。最后使用CAS改变state的值,保证原子性。

那么AbstractQueuedSynchronizer通过更新state的值来实现的加锁和解锁。

下面是关键源代码的截图。
请添加图片描述
请添加图片描述


1.2、任务队列

AQS中维护了一个任务队列,是一个双向队列。队列节点是内部类Node

Node中记录者节点的状态waitStatus,比如CANCELSIGNAL等分别表示该任务节点已经取消和任务节点正在沉睡需要被唤醒。

当然,因为是双向列表所以也有指向前后节点的指针。下面是Node源码的部分截图。
请添加图片描述

这个队列会初始化一个头结点和一个尾结点作为虚拟节点。头结点的状态在整个加锁和释放锁的过程中都会变化。

1.2.1、结点的状态变化

当头结点指向的Node才拥有锁。

这里主要介绍三个状态

  • 0, 表示当前Node后续无节点在排队。不表明是否拥有锁。
  • -1,表示除了当前Node在排队以外,还有其他Node排在当前Node后面。不表明是否拥有锁。
  • 1,表示当前Node可能因为等待时间太长而放弃获取锁。

下面是三个Node在队列中的状态。这里从左到右解释他们的状态。
请添加图片描述
head指向第一个Node,所以当前Node拥有锁。

第一个NodewaitStatus=-1表示后续有节点等待获取锁。当该节点释放锁时会唤醒后续的节点。

第二个NodewaitStatus = -1,后续有节点等待获取锁。

第三个NodewaitStatus = 0,后续无节点等待获取锁。

1.3、加锁和解锁的简单流程

假设有两个线程A和B,他们需要争夺基于AQS实现的锁,下面是争夺的简单流程。

  1. 线程A先执行CAS,将state从0修改为1,线程A就获取到了锁资源,去执行业务代码即可。
  2. 线程B再执行CAS,发现state已经是1了,无法获取到锁资源。
  3. 线程B需要去排队,将自己封装为Node对象。
  4. 需要将当前B线程的Node放到双向队列保存,排队。

2、ReentrantLock

2.1、加锁源码分析

ReentrantLock分为公平锁和非公平锁。在加锁的时候因这两种锁的不同会有不同的加锁方式。

ReentrantLock默认是非公平锁,构造方法中传入false则是公平锁。

非公平锁的lock()方法会直接基于CAS尝试获取锁,如果成功的话则执行setExclusiveOwnerThread()方法表示当前线程持有该锁;如果失败则执行acquire()方法。

公平锁则是直接执行acquire()方法。下面是源码对比。
请添加图片描述
接下来的重点则是看acquire()的具体操作。

tryAcquire()方法会再次尝试获取锁,如果成功返回true,否则返回false

可以看到如果失败的话则将请求放到等待队列中同时发送中断信号。
在这里插入图片描述

2.1.1、tryAcquire()的具体实现

  • 非公平锁
    非公平锁会尝试再次直接通过CAS获取锁资源。因为是可重入锁,所以当锁的持有者是当前线程时也可直接获取锁,然后计数器加一。
    请添加图片描述

  • 公平锁
    公平锁的逻辑与非公平锁类似,只不过再获取锁之前会先判断AQS中自己是不是排在第一位,之后才会获取锁。
    请添加图片描述

2.1.2、acquirQueued()的具体实现

在这里插入图片描述
tryAcquire()返回false,即获取锁失败,就开始尝试将当前线程封装成Node节点插入到AQS的结尾。

在插入时我们会看到if(p == head && tryAcquire(arg))这样的语句。

这是因为AQS有伪头结点,所以当这个线程插入到AQS中时发现自己的上一个节点是头结点,即自己排在第一位,那无论是公平锁还是非公平锁自己都可以再次测试获取锁。所以会再次执行tryAcquire()

final boolean acquireQueued(final Node node, int arg) {// 不考虑中断// failed:获取锁资源是否失败(这里简单掌握落地,真正触发的,还是tryLock和lockInterruptibly)boolean failed = true;try {boolean interrupted = false;for (;;) {// 拿到当前节点的前继节点final Node p = node.predecessor();// 前继节点是否是head,如果是head,再次执行tryAcquire尝试获取锁资源。if (p == head && tryAcquire(arg)) {// 获取锁资源成功setHead(node);p.next = null; // 获取锁失败标识为falsefailed = false;return interrupted;}// 没拿到锁资源……// shouldParkAfterFailedAcquire:基于上一个节点转改来判断当前节点是否能够挂起线程,如果可以返回true,// 如果不能,就返回false,继续下次循环if (shouldParkAfterFailedAcquire(p, node) &&// 这里基于Unsafe类的park方法,将当前线程挂起parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)// 在lock方法中,基本不会执行。cancelAcquire(node);}
}

2.1.3、tryLock的具体实现

无参的tryLock()比较简单,和tryAcquire()基本没区别。

这里主要讲解有参的tryAcquireNanos(int arg, long nanosTimeout)

它的作用在一个时间内尝试获得锁。在这个时间内没有获得锁会挂起park线程。如果成功则返回true,时间结束还没有获得则返回false

public boolean tryLock(long timeout, TimeUnit unit)throws InterruptedException {return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

该方法需要处理中断异常,和lock()方法不一样。

我们继续深入。

public final boolean tryAcquireNanos(int arg, long nanosTimeout)throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();return tryAcquire(arg) ||doAcquireNanos(arg, nanosTimeout);
}

可以看到,它直接通过线程的中断标志位决定是否抛出异常。

之后进行tryAcquire(),这个方法细节上面分析过,它有公平和非公平两种实现,简而言之就是非公平直接尝试CAS加锁,公平则是进入队列排队。

也就是说,最后它会正常加锁,只有失败时才会执行doAcquireNanos()。所以有参的tryLock()方法park线程的细节就在其中。

那下面就看看这个方法的内部。

核心就是线程会被封装Node放到队列中,之后查看时间,如果时间比较长,就park线程直到时间结束后再尝试获取锁;如果时间比较短,就在死循环中等到时间结束然后再次获得锁。

因为park的线程主要会因两个动作结束park,即时间到,或者线程发出中断状态,所以最后会查看park是因为什么结束的。如果是中断则抛出异常,否则尝试获取锁。

private boolean doAcquireNanos(int arg, long nanosTimeout)throws InterruptedException {// 如果等待时间是0秒,直接告辞,拿锁失败  if (nanosTimeout <= 0L)return false;// 设置结束时间。final long deadline = System.nanoTime() + nanosTimeout;// 先扔到AQS队列final Node node = addWaiter(Node.EXCLUSIVE);// 拿锁失败,默认trueboolean failed = true;try {for (;;) {// 如果在AQS中,当前node是head的next,直接抢锁final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return true;}// 结算剩余的可用时间nanosTimeout = deadline - System.nanoTime();// 判断是否是否用尽的位置if (nanosTimeout <= 0L)return false;// shouldParkAfterFailedAcquire:根据上一个节点来确定现在是否可以挂起线程if (shouldParkAfterFailedAcquire(p, node) &&// 避免剩余时间太少,如果剩余时间少就不用挂起线程nanosTimeout > spinForTimeoutThreshold)// 如果剩余时间足够,将线程挂起剩余时间LockSupport.parkNanos(this, nanosTimeout);// 如果线程醒了,查看是中断唤醒的,还是时间到了唤醒的。if (Thread.interrupted())// 是中断唤醒的!throw new InterruptedException();}} finally {if (failed)cancelAcquire(node);}
}

2.1.5、总结

ReentrantLock的加锁有公平锁和非公平锁两种方式。

对于非公平锁,任务一开始会直接尝试通过CAS获取锁,失败后才会进入任务队列。并且进入的时候会再次尝试获取锁。整个过程并不考虑其他节点等了多久,所以才是非公平锁。

对于公平锁,任务会按序先进入任务队列,直到有人唤醒他们才会开始获取锁。


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

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

相关文章

在C++11及后续标准中,auto和decltype是用于类型推导的关键特性,它们的作用和用法。

在C11及后续标准中&#xff0c;auto和decltype是用于类型推导的关键特性&#xff0c;它们的作用和用法有所不同。以下是详细说明&#xff1a; 1. auto 关键字 基本作用 自动推导变量的类型&#xff08;根据初始化表达式&#xff09;主要用于简化代码&#xff0c;避免显式书写…

Linux:进程程序替换execl

目录 引言 1.单进程版程序替换 2.程序替换原理 3.6种替换函数介绍 3.1 函数返回值 3.2 命名理解 3.3 环境变量参数 引言 用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支)&#xff0c;我们所创建的所有的子进程&#xff0c;执行的代码&#x…

LeetCode.02.04.分割链表

分割链表 给你一个链表的头节点 head 和一个特定值 x &#xff0c;请你对链表进行分隔&#xff0c;使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。 你不需要 保留 每个分区中各节点的初始相对位置。 示例 1&#xff1a; 输入&#xff1a;head [1,4,3,2,5,2], x …

Johnson算法 流水线问题 java实现

某印刷厂有 6项加工任务J1&#xff0c;J2&#xff0c;J3&#xff0c;J4&#xff0c;J5&#xff0c;J6&#xff0c;需要在两台机器Mi和M2上完 成。 在机器Mi上各任务所需时间为5,1,8,5,3,4单位; 在机器M2上各任务所需时间为7,2,2,4,7,4单位。 即时间矩阵为&#xff1a; T1 {5, …

按键++,--在操作uint8_t类型(一个取值为1~10的数)中,在LCD中显示两位数字问题

问题概况 在执行按键&#xff0c;--过程中&#xff0c;本来数值为1~10.但是在执行过程中&#xff0c;发现数值在经过10数值后&#xff0c;后面的“0”会一直在LCD显示屏中显示。 就是执行操作中&#xff0c;从1&#xff0c;2&#xff0c;3&#xff0c;4&#xff0c;5&#xf…

【QT】QTreeWidgetItem的checkState/setCheckState函数和isSelected/setSelected函数

目录 1、函数原型1.1 checkState/setCheckState1.2 isSelected/setSelected2、功能用途3、示例QTreeWidget的checkState/setCheckState函数和isSelected/setSelected这两组函数有着不同的用途,下面具体说明: 1、函数原型 1.1 checkState/setCheckState Qt::CheckState QTr…

005 vue项目结构 vue请求页面执行流程(vue2)

文章目录 vue项目结构vue请求页面执行流程main.jsrouterHelloWorld.vueApp.vueindex.html vue项目结构 config目录存放的是配置文件&#xff0c;比如index.js可以配置端口 node_modules存放的是该项目依赖的模块&#xff0c;这些依赖的模块在package.json中指定 src目录分析 1…

汇丰xxx

1. Spring Boot 的了解&#xff0c;解决什么问题&#xff1f; 我的理解&#xff1a; Spring Boot 是一个基于 Spring 框架的快速开发脚手架&#xff0c;它简化了 Spring 应用的初始搭建和开发过程。解决的问题&#xff1a; 简化配置&#xff1a; 传统的 Spring 应用需要大量的…

基于 Spring Boot 瑞吉外卖系统开发(一)

基于 Spring Boot 瑞吉外卖系统开发&#xff08;一&#xff09; 系统概述 系统功能 技术选型 初始项目和数据准备 初始项目和SQL文件下载 创建数据库并导入数据 打开reggie项目 运行效果 主函数启动项目&#xff0c;访问URL&#xff1a; http://127.0.0.1:8080/backend/pag…

大型语言模型智能应用Coze、Dify、FastGPT、MaxKB 对比,选择合适自己的LLM工具

大型语言模型智能应用Coze、Dify、FastGPT、MaxKB 对比&#xff0c;选择合适自己的LLM工具 Coze、Dify、FastGPT 和 MaxKB 都是旨在帮助用户构建基于大型语言模型 (LLM) 的智能应用的平台。它们各自拥有独特的功能和侧重点&#xff0c;以下是对它们的简要对比&#xff1a; Coz…

【项目管理】第6章 信息管理概论 --知识点整理

项目管理 相关文档&#xff0c;希望互相学习&#xff0c;共同进步 风123456789&#xff5e;-CSDN博客 &#xff08;一&#xff09;知识总览 项目管理知识域 知识点&#xff1a; &#xff08;项目管理概论、立项管理、十大知识域、配置与变更管理、绩效域&#xff09; 对应&…

Zapier MCP:重塑跨应用自动化协作的技术实践

引言&#xff1a;数字化协作的痛点与突破 在当今多工具协同的工作环境中&#xff0c;开发者与办公人员常常面临数据孤岛、重复操作等效率瓶颈。Zapier推出的MCP&#xff08;Model Context Protocol&#xff09;协议通过标准化数据交互框架&#xff0c;为跨应用自动化提供了新的…

echart实现动态折线图(vue3+ts)

最近接到个任务&#xff0c;需要用vue3实现动态折线图。之前没有用过&#xff0c;所以一路坎坷&#xff0c;现在记录一下&#xff0c;以后也好回忆一下。 之前不清楚echart的绘制方式&#xff0c;以为是在第一秒的基础上绘制第二秒&#xff0c;后面实验过后&#xff0c;发现并…

Java学习——day24(反射进阶:注解与动态代理)

文章目录 1. 反射与注解2. 动态代理3. 实践&#xff1a;编写动态代理示例4. 注解定义与使用5. 动态代理6. 小结与思考 1. 反射与注解 注解&#xff1a;注解是 Java 提供的用于在代码中添加元数据的机制。它不会影响程序的执行&#xff0c;但可以在运行时通过反射获取和处理。反…

C++之nullptr

文章目录 前言 一、NULL 1、代码 2、结果 二、nullptr 1、代码 2、结果 总结 前言 当我们谈论空指针时,很难避免谈及nullptr。nullptr是C++11引入的一个关键字,用来表示空指针。在C++中,空指针一直是一个容易引起混淆的问题,因为在早期版本的C++中,通常使用NULL来…

JavaScript惰性加载优化实例

这是之前的一位朋友的酒桌之谈&#xff0c;他之前负责的一个电商项目&#xff0c;刚刚开发万&#xff0c;首页加载时间特别长&#xff0c;体验很差&#xff0c;所以就开始排查&#xff0c;发现是在首页一次性加载所有js导致的问题&#xff0c;这个问题在自己学习的时候并不明显…

苹果内购支付 Java 接口

支付流程&#xff0c;APP支付成功后 前端调用后端接口&#xff0c;后端接口将前端支付成功后拿到的凭据传给苹果服务器检查&#xff0c;如果接口返回成功了&#xff0c;就视为支付。 代码&#xff0c;productId就是苹果开发者后台提前设置好的 产品id public CommonResult<S…

数据库中的数组: MySQL与StarRocks的数组操作解析

在现代数据处理中, 数组 (Array) 作为一种高效存储和操作结构化数据的方式, 被广泛应用于日志分析, 用户行为统计, 标签系统等场景. 然而, 不同数据库对数组的支持差异显著. 本文将以MySQL和StarRocks为例, 深入解析它们的数组操作能力, 并对比其适用场景. 文章目录 一 为什么需…

LeetCode零钱兑换(动态规划)

题目描述 给你一个整数数组 coins &#xff0c;表示不同面额的硬币&#xff1b;以及一个整数 amount &#xff0c;表示总金额。 计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额&#xff0c;返回 -1 。 你可以认为每种硬币的数量是无…

/sys/fs/cgroup/memory/memory.stat 关键指标说明

目录 1. **total_rss**2. **total_inactive_file**3. **total_active_file**4. **shmem**5. **其他相关指标**总结 以下是/sys/fs/cgroup/memory/memory.stat文件中一些关键指标的详细介绍&#xff0c;特别是与PostgreSQL相关的指标&#xff1a; 1. total_rss 定义&#xff1…