ReentrantLock原理剖析

news2024/12/26 2:21:40

前言

本文主要讲解底层逻辑,基本不会贴代码,目的是让大家能够真正的知晓原理,对照着逻辑去理解代码看代码也会很快就能看懂。
在讲ReentrantLock原理之前,我们先回顾下ReentrantLock的基本用法。ReentrantLock是一个锁编程api,让我们达到类似syncronized类似的效果,但它可以提供更多的功能,例如公平锁等。通常的写法如下所示:

class X {    
private final ReentrantLock lock = new ReentrantLock();   

public void m() {     
    doSomething();
    
    // 需要加锁的代码块 
	lock.lock();  // 加锁   
	
	try {        
		// 业务处理
	} finally {        
		lock.unlock() // 释放锁,一定要放在finalyy块中
		}    
	}  
}

ReentrantLock的重点

公平和非公平区别在哪里?

理解ReentrantLock一切的起点从暴露出来的接口开始。接下来,我们探索的入口就是从这个lock()方法开始:
在这里插入图片描述
内部调用的是sync.lock(),我们经常讲的ReentrantLock有公平锁和非公平锁两种实现,这里的sync就是非公平同步器和公平同步器之一。为了高性能,ReentrantLock 默认采用了非公平锁实现方式。相关类的继承关系如下所示:
在这里插入图片描述
RentrantLock实际上是一层包装,根据公平和非公平需要,持有相应的公平或者非公平的同步器。内部基本所有的功能都是通过Sync及其子类实现的,Sync的lock方法是一个抽象方法,实现是通过子类实现的,公平锁和非公平锁lock操作的流程如下所示:
在这里插入图片描述
非公平锁加锁流程会先有一个cas的操作尝试获取锁,成功了就执行业务代码块,不用再去排队。如果cas失败,就走正常的尝试获取锁、入队流程。因为少了入队而产生的阻塞、唤醒,使得非公平锁的性能很好。这样也会带来一个问题,就是如果新来的线程总是能抢到锁的话,队列里的线程就因为老是抢不到锁而处于饥饿状态。
在这里插入图片描述
公平锁加锁流程就比较简单了,直接进行尝试获取锁、入队流程。这里的尝试获取锁里面有特殊判断(接下来马上会讲),不会造成非公平的现象,可以理解不管谁来都老老实实的去排队。
公平锁和非公平锁在尝试获取锁的操作上面会有一丝不同。如果是非公平锁,会直接通过cas再次尝试加锁(够无赖的)。而公平锁会首先判断队列里面是否已经有等待的线程了,如果有则老老实实去排队,如果没有,那就各凭本事去抢锁了,很公平。两个tryAcquire的流程如下所示:
在这里插入图片描述
在这里插入图片描述
小结:非公平锁加锁流程会先有一个cas的操作尝试获取锁,成功了就执行业务代码块,不用再去排队。如果cas失败,就走正常的尝试获取锁、入队流程。公平锁加锁会判断是否有线程在队列中,如果没有则直接进行尝试获取锁,成功了就执行业务代码块,失败则开始入队。如果有线程已经在队列中,则进行入队。

可重入的实现方式

可重入性就是一个线程不用释放,可以重复的获取一个锁n次,只是在释放的时候,也需要相应的释放n次。可重入锁的实现解决了同一个线程需要多次获取同一个锁的问题。
要实现可重入锁,至少需要保存两个东西:现在持有锁的线程已经上锁的次数。刚才讲上锁流程的时候,我们已经提到过两个对象exclusiveOwnerThread和state。exclusiveOwnerThread保存了持有锁的线程,state在>0的情况下保存了线程对锁的重入次数,这样在释放的时候减去相应的次数即可,等state=0时就说明已经完全释放掉了,这时候再将exclusiveOwnerThread设置为null。
在这里插入图片描述
到此为止属于ReentrantLock的内容实际上基本就结束了,剩下的部分全部是AQS实现的,所以我们的重点要转向去研究AQS的结构和流程。

AQS

AQS的结构

AQS的结构不难,我们要做到的就是把下面这个图能自己闭着眼睛画出来。
在这里插入图片描述
我们上面讲的获取锁,释放锁,操作的就是这个state对象。state的含义并非固定的,在ReentrantLock中,state初始化的时候是0,表示没有上锁,=1表示有线程对它加了锁,>1表示重入了多少次。在CountDownLatch中,它表示有多少个任务需要等待完成。ReentrantLock中state状态流转如下所示:
在这里插入图片描述
AQS的所有结点都是Node对象,Node对象结构在上面的类图里面已经画出来了,我们需要初步的理解它:
pre/next:没什么好说的,指向前驱、后驱的结点指针。
thread:保存了入队列的线程。
waitStatus:重点理解对象!!waitStatus包括CANCELLED(线程已经被删除了)、SIGNAL(后面的线程需要被唤醒)、CONDITION(线程在等待condition条件)、PROPAGATE(共享锁需要传递),先了解即可。
nextWaiter:指向下个等待条件的Node对象,如果不是Condition的对象,默认是Node.EXCLUSIVE,表示这是个排他锁。

在有线程获取锁失败入队的情况下,整个AQS的结构大概就是这个样子:
在这里插入图片描述

入队流程

以非公平锁为例,尝试加锁失败后会进入到AQS队列中。队列的对象都是Node对象,所以加入队列前会根据这个线程创建一个Node对象,Node结点的nextWaiter设置成了Node.EXCLUSIVE,然后将该结点入队,流程如下所示:
在这里插入图片描述
对应代码为private Node addWaiter(Node mode)。

队列中的线程怎么抢锁

线程入队了,那么队列中的线程怎么抢锁呢?看起来似乎很简单,既然都在队列中了,那肯定就是队列头的线程先拿到锁呗。但是我们需要考虑几个问题:
1、队列中的线程抢锁什么时候触发?
2、我们知道线程没有抢到锁会被阻塞,轮到它时,该怎么被唤醒?
3、队列中如果有无效的线程怎么办?
我们带着这几个问题去看看队列的处理。
实际上,在线程抢锁失败后包装成一个Node而入队,在入队完成后,紧接着就会触发队列抢锁操作,如下所示:
在这里插入图片描述
现在我们知道队列的中的线程抢锁,实际上是从入队的这个线程触发的,整个处理的流程被包在了一个死循环里面,具体的流程如下所示:
在这里插入图片描述
在初始化队列的时候,我们设置的了head,tail,如果入队的这个线程是第一个线程,那么它的前驱结点刚好就是head,说明它在队列头,该它去抢锁了。如果不是,理论上该线程就应马上被阻塞,但是阻塞前会做一些额外判断工作,这个时候我们的waitStatus就排上用场了!

waitStatus

waitStatus包括CANCELLED(1:线程已经被删除了)、SIGNAL(-1:后面的线程需要被唤醒)、CONDITION(-2:线程在等待condition条件)、PROPAGATE(-3:共享锁需要传递),状态流转如下所示:
在这里插入图片描述
Condition和Propogate两种状态分别用于lock.newConditon和共享锁,这里暂不涉及。阻塞前的waitStatus判断和处理如下所示:
在这里插入图片描述
当需要被阻塞的线程设置好了“哨兵”后,就放心的调用LockSupport.park方法阻塞自己。那么B什么时候会被唤醒呢?当A线程业务处理完成后,调用unlock方法释放琐,里面就会唤醒后续的线程,流程如下所示:
在这里插入图片描述
释放锁跟加锁差不多的操作,重点就在这个唤醒上面,唤醒的流程如下所示:
在这里插入图片描述
因为头结点是一个dummy结点,现在要唤醒它后面的线程,waitStatus需要重新恢复成0的状态。然后拿到后继线程结点,这个拿很有讲究,首先是通过h.next尝试获取,然后看看是否能拿到。如果能拿到而且是正常阻塞的线程,那万事大吉直接唤醒它。如果不幸是空或者已经删除了,则从tail往前找啊找,找到最前面的一个正常结点(没有破坏FIFO的性质,waitStatus < 0为正常状态,这里情况就是SIGNAL。队列中如果有无效的线程就会被跳过),然后唤醒它。关于为什么采用tail往前遍历的做法,可以参考这个:https://www.zhihu.com/question/50724462?sort=created。
唤醒线程后,线程就继续在死循环里去抢锁了。
对应的代码在final boolean acquireQueued(final Node node, int arg)中。

condition怎么玩的?

condition用于线程同步编程,实际上它拥有自己的队列,限于篇幅这个放在另外的文章里。

总结

ReentrantLock是最常用的锁API,在线程池ThreadPoolExecutor、阻塞队列LinkedBlockingQueue,同步工具CyclicBarrier等中都能看到它的身影。掌握好ReentrantLock的原理,可以让我们更加清楚基于它的实现的其他工具类的实现原理。

ReentrantLock实现了类似Syncronized的同步能力,但它还支持了公平锁的能力。根据公平和非公平需要,ReentrantLock持有相应的公平或者非公平的同步器。非公平锁加锁流程会先有一个cas的操作尝试获取锁,成功了就执行业务代码块,不用再去排队。如果cas失败,就走正常的尝试获取锁、入队流程。公平锁尝试加锁会判断是否有线程在队列中,如果没有则直接进行尝试获取锁,成功了就执行业务代码块,失败则开始入队。如果有线程已经在队列中,则进行入队操作。NonfairSync和FairSync的区别体现在lock和tryAcquire两个方法上面。

ReentrantLock通过exclusiveOwnerThread和state来实现可重入,exclusiveOwnerThread保存了持有锁的线程,state在>0的情况下保存了线程对锁的重入次数,这样在释放的时候减去相应的次数即可,等state=0时就说明已经完全释放掉了,这时候exclusiveOwnerThread设置为null。

AQS需要对队列和Node的类结构铭记于心,尤其是state和waitStatus含义,理解了它俩,上锁和释放锁流程就会变得非常容易。

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

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

相关文章

Compiler Lab2- 自制极简编译器

笔者实现的这个超级迷你版编译器&#xff08;词法分析、语法分析、生成中间代码&#xff08;cpp&#xff09;&#xff09;仅支持四则运算&#xff0c;功能真的是非常非常简单&#xff0c;不过其中的流程&#xff08;词法分析 -> 语法分析 -> 中间代码生成&#xff09;还是…

各SQL引擎的SQL转换过程对比

SQL引擎 参考文档:高级语言的解析过程—解析树 从 MySQL、Oracle、TiDB、CK,到 Hive、HBase、Spark,从关系型数据库到大数据计算引擎,他们大都可以借助 SQL 引擎,实现 “接受一条 sql 语句然后返回查询结果” 的功能。 他们核心的执行逻辑都是一样的,大致可以通过下面…

【五一创作】《嵌入式系统》知识总结7:GPIO寄存器

总述 每组端口具有7个寄存器 • 实现对GPIO端口初始化配置和数据输入输出控制 1. 配置寄存器&#xff1a;GPIOx_CRL、GPIOx_CRH 用来选择引脚功能&#xff0c;例如输入或输出 2. 数据寄存器&#xff1a;GPIOx_IDR、GPIOx_ODR 用来保存引脚输入电平或输出电平 3. 位控寄存器…

数据库管理-第七十一期 五一,休息?(20230503)

数据库管理 2023-05-03 第七十一期 五一&#xff0c;休息&#xff1f;1 备份2 两个DDL3 问题处理4 问题排查总结 第七十一期 五一&#xff0c;休息&#xff1f; 好不容易&#xff0c;熬过万恶的6天班来到了五一假期&#xff0c;想着好好休息&#xff0c;顺便把绝地幸存者给通关…

Java 基础进阶篇(九)—— 常用 API

文章目录 一、Object 类二、Objects 工具类三、Math 类四、System 类五、BigDecimal 类 一、Object 类 一个类要么默认继承了 Object 类&#xff0c;要么间接继承了 Object 类&#xff0c;Object 类是 java 中的祖宗类。Object 类的方法是一切子类都可以直接使用的。 因此&…

Obsidian +Obsidian Git插件 + Gitee 自动同步笔记

在Obsidian 关闭安全模式 然后再插件市场里面搜索并下载Obsidian Git 这个插件 注意&#xff1a;这里需要科学上网才能搜索并下载&#xff0c;我看很多博主都没说这件事情 然后再你的Gitee中&#xff0c;新建一个仓库 把这两个勾选上&#xff0c;然后随便选个语言&#xff0…

【JavaEE】应用层UDP协议

博主简介&#xff1a;想进大厂的打工人博主主页&#xff1a;xyk:所属专栏: JavaEE初阶 本篇文章将为大家介绍应用层中UDP协议~~ 在应用层这里&#xff0c;虽然存在一些现有的协议&#xff08;HTTP&#xff09;&#xff0c;但是也有很多情况&#xff0c;需要程序猿自定制协议&a…

Windeployqt 打包,缺少DLL 的原因分析,解决方法

很多同学使用工具windeployqt进行打包发布后&#xff0c;运行exe文件时&#xff0c;还是会出现下图所示的系统错误提示&#xff0c;这种情况就表示相关的DLL 库文件没有被正确打包。可是windeployqt明确显示运行正常啊&#xff0c;难道是QT自家的windeployqt这个工具有bug&…

2024年浙大MBA创客班项目提面如何申请?

2024年浙大MBA创客班提面如何申请&#xff1f;在目前的提前批面试申请过程中不少考生都不太清楚&#xff0c;专注浙大的杭州达立易考教育本期将项目的提前批面试和常规批复试申请基本流程和关键信息整理出来&#xff0c;帮助考生更精准的进行面试准备。 一、浙大MBA创客班项目…

基于比较排序算法总结

1.数据无法全部放入内存 2.数据以数据流形式

操作系统2(多处理器编程)

一、并发 1.操作系统是最早的并发程序之一 2.并发的基本单位&#xff1a;线程 共享内存的多个执行流 执行流拥有独立的堆栈/寄存器共享全部的内存&#xff08;指针可以互相引用&#xff09; 3.实现原子性 lock(&lk)unlock(&lk) 实现临界区(critical section)之间…

keepalived脑裂现象

Keepealived最常见的问题是会出现脑裂现象&#xff1a; Master一直发送心跳消息给backup主机&#xff0c;如果中间的链路突然断掉&#xff0c;backup主机将无法收到master主机发送过来的心跳消息&#xff08;也就是vrrp报文&#xff09;&#xff0c;backup这时候会立即抢占mas…

Snmputil和Snmputilg工具的下载和基本使用 SNMP协议 Windows系统SNMP服务的安装教程

⬜⬜⬜ ---&#x1f7e7;&#x1f7e8;&#x1f7e9;&#x1f7e6;&#x1f7ea; (*^▽^*)欢迎光临 &#x1f7e7;&#x1f7e8;&#x1f7e9;&#x1f7e6;&#x1f7ea;---⬜⬜⬜ ✏️write in front✏️ &#x1f4dd;个人主页&#xff1a;陈丹宇jmu &#x1f381;欢迎各位→…

现代CMake高级教程 - 第 7 章:变量与缓存

双笙子佯谬老师的【公开课】现代CMake高级教程课程笔记 第 7 章&#xff1a;变量与缓存 重复执行 cmake -B build 会有什么区别&#xff1f; ❯ cmake -B build -- The C compiler identification is GNU 11.3.0 -- The CXX compiler identification is GNU 11.3.0 -- Detec…

C++:分治算法之输油管道问题

目录 描述 输入 输出 输入样例 输出样例 分析 代码 运行结果 描述 ¢ 某石油公司计划建造一条 由东向西 的主输油管道。该管道要穿过一个有n口油井的油田。从每口油井都要有一条输油管道沿最短路经&#xff08;或南或北&#xff09;与主管道相连。 ¢ 如果给定…

如何区分GPT3.5和4?

切换模型 前两天申请的GPT 4的API调用权限终于申请下来了。 这两天我也是抓紧开发&#xff0c;让自己搭建的国内网站&#xff08;aichatroom.cn&#xff09;可以快速支持上使用GPT 4。 GPT 3.5和GPT4的区别 GPT-3.5 和 GPT-4 分别代表了 OpenAI 发布的两个不同版本的自然语言处…

现代CMake高级教程 - 第 4 章:对象的属性

双笙子佯谬老师的【公开课】现代CMake高级教程课程笔记 第 4 章&#xff1a;对象的属性 除了 POSITION_INDEPENDENT_CODE 还有哪些这样的属性&#xff1f; add_executable(main main.cpp)set_property(TARGET main PROPERTY CXX_STANDARD 17) # 采用 C17 标准进行编译&am…

STC15W104 8脚单片机串口下载程序

单片机串口下载是一种常见的单片机程序下载方式&#xff0c;它通过串口线连接单片机的串口引脚和电脑的串口接口实现。下面是单片机串口下载的基本原理和操作方法&#xff1a; 原理 确定下载模式&#xff1a;大多数单片机芯片都支持串口下载模式&#xff0c;需要在程序中设置…

常识性概念图谱建设与应用

目录 一、知识图谱背景介绍 &#xff08;一&#xff09;基本背景 &#xff08;二&#xff09;与NLP的关系 &#xff08;三&#xff09;常识性概念图谱的引入对比 二、常识性概念图谱介绍 &#xff08;一&#xff09;常识性概念图谱关系图示例 &#xff08;二&#xff09…

深度学习-tensorflow 使用keras进行深度神经网络训练

概要 深度学习网络的训练可能会很慢、也可能无法收敛&#xff0c;本文介绍使用keras进行深度神经网络训练的加速技巧&#xff0c;包括解决梯度消失和爆炸问题的策略&#xff08;参数初始化策略、激活函数策略、批量归一化、梯度裁剪&#xff09;、重用预训练层方法、更快的优化…