【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码

目录

前言:

前置知识: 

什么是公平锁与非公平锁?

尝试自己构造一把锁:

ReentrantLock源码:

加锁:

解锁:

总结:


 

前言:

在并发编程中,线程安全是一个重要的问题。为了保证多个线程之间的互斥访问和正确的同步操作,Java提供了一种强大的锁机制——ReentrantLock(可重入锁)。

与synchronized相比,ReentrantLock提供了更加灵活和强大的功能。它支持公平锁和非公平锁两种模式,可以通过lock()和unlock()方法手动控制锁的获取和释放,并且可以实现可重入特性,即同一个线程可以多次获得同一个锁而不会发生死锁。

在本文中,我们将详细讲解ReentrantLock的使用方法,包括基本的锁操作、可重入特性、公平性设置、条件变量和中断等。我们还将对比ReentrantLock和synchronized的性能差异,并给出一些适用场景的建议。

在正式介绍ReentrantLock之前,我们要先来学习一些前置知识,方便大家更好的理解ReentrantLock源码。

前置知识: 

什么是公平锁与非公平锁?

公平锁:

        公平锁是指多个线程按照申请锁的顺序依次获取锁,即先到先得的原则。当一个线程释放锁后,等待时间最长的那个线程将获得锁的访问权。公平锁可以避免线程饥饿,保证每个线程都有机会获得锁,但可能会降低系统的整体吞吐量。

非公平锁:

        非公平锁是指多个线程竞争锁时,不考虑线程的等待时间,直接尝试获取锁。如果当前锁没有被其他线程持有,则该线程立即获得锁的访问权,即先到先得。非公平锁的性能相对较好,因为它减少了线程切换和调度的开销,但可能会导致某些线程一直无法获取到锁,产生线程饥饿的问题。

简而言之:公平锁在线程获取锁的时候,等待时间最长的线程获取锁的访问权。非公平锁是线程自由竞争,不存在按照等待时间排序。

而ReentrantLock既可以是公平锁,也可以是非公平锁。无参构造情况下是非公平锁

尝试自己构造一把锁:

锁的基本原理就是:加锁就是放行或者阻塞当前线程。放锁就是唤醒被阻塞的线程。

基于这个思想,我们很快就可以设计出一个最简单的锁:

public class LiLocks {int statue =0;public void lock(){while ( statue!=0){System.out.println(Thread.currentThread().getName()+"等待锁");}statue=1;}public void unlock(){statue=0;}
}

但是这个锁存在一个最基本的问题:并不能处理全部情况

        理想状态下,A线程进入lock之后,先判断当前statue==0,之后做statue自增操作。这样当下一个线程进入的时候就会被阻塞到while循环中,直到A线程进入unlock方法对statue做递减操作。如果statue==0的话,就相当于释放锁。

而在有些特殊情况下,当A线程比较完之后,还没来得及给statue赋值,另外一个B线程就抢到了资源调度,由于此时statue还没变值,所以此时B线程仍然可以通过比较不进入while循环。就造成了加锁失败的情况

 也就是说:如果我们想要优化这个锁,那么就需要使得判断statue==0和赋值statue=1具有原子性。

这两步就是CAS(Compare And Swap),只不过我们的CAS是自己写的,所以不具备原子性。那么我们调用Java为我们提供的具有原子性的CAS操作就好了。

而Java为我们提供的具有原子性的cas操作来自Unsafe类,那么我们使用Unsafe类来改造代码:

public class LiLock {private static Unsafe unsafe;volatile  int statue =0;static {try {Field field = Unsafe.class.getDeclaredField("theUnsafe");field.setAccessible(true);unsafe = (Unsafe) field.get(null);} catch (Exception e) {e.printStackTrace();}}long objectOffset = unsafe.objectFieldOffset(LiLock.class.getDeclaredField("statue"));public LiLock() throws NoSuchFieldException {}public void lock() throws NoSuchFieldException {while (!unsafe.compareAndSwapInt(this, objectOffset, 0, 1)){System.out.println(Thread.currentThread().getName()+" is waiting");}}public void unlock() {statue=0;}
}

在这里,我们简单实现了一个自旋锁。而我们如果想要把这个锁改为公平锁,要怎么做呢?

也就是说:当此时有1,2,3 总共三个线程的时候,如果1线程获取到了锁,那么我们如何让2,3线程按照顺序获取锁呢?

假设线程2此时已经被while循环阻塞了,如果此时线程3来的话,那么我们是不能让线程3进入while阻塞的。如果3也被阻塞的话,那么不就是说2和3又在同一起跑线了。无法保证2先来就先获取到锁。

所以此时的问题就在于:如何不使用相同的while来阻塞3线程,让2线程优先获得到锁?

我们回顾一下我们之前学过的阻塞一个线程的方式:

1.wait():使用不了,他需要和synchronized 关键字一起使用,而我们是在自己写锁,当然不能使用synchronized

2.sleep():使用不了,我们不知道线程1需要多长时间才会释放锁,因此无法确定让线程2,3睡眠多长时间。

3.while(ture):使用不了,使用这个还是将线程2,3放到一个起跑线了,无法确保先来的线程2一定能过比线程3获取到锁

基于这三种方法都使用不了,我们只能采用第四种:LockSupport类中的park()方法,它可以阻塞一个线程,使其不消耗cpu资源,直至被其他线程唤醒或者中断

那么我们直接对线程3采用park方法就可以了。通过这种机制,我们就实现了公平锁的机制。

通过上述知识,我们引入了公平锁的基本实现方式,理解如何构造一个公平锁有利于我们阅读ReentrantLock源码。

ReentrantLock源码:

加锁:

        最外层:

这段代码的基本逻辑为:如果当前线程不能获取锁,则会进入等待队列中等待,并在等待过程中不断尝试获取锁。如果当前线程被中断,则会重新设置中断标志。

我们在这里主要聊的是加锁,因此我们就先看tryAcquire这个方法:

这个方法分为两个实现:公平锁非公平锁,因此我们逐个来看:

公平锁:

我们来先看一下这个方法中调用的一些比较重要的方法:

1.setExclusiveOwnerThread():将锁的拥有线程修改为当前线程。

2.compareAndSetState():具有原子性的CAS操作。

3.getExclusiveOwnerThread():获取当前持有锁的线程对象。

4.setState:设置当前锁的状态。 

5.hasQueuedPredecessors():判断当前线程是否需要排队

在了解完这些方法之后,其实了解公平锁的代码逻辑就很简单了。

公平锁的基本代码逻辑为:

1.获取当前线程和锁状态。

2.如果当前锁没有被线程获取(c==0),进一步判断:当前线程是否需要排队(!hasQueuedPredecessors()),如果不需要就进行CAS操作,将状态从0变为acquirs。并且设置锁的持有线程为当前线程。返回结果为true。

3.如果当前锁已经被线程获取,则再次判断获取锁的线程是不是当前线程(current == getExclusiveOwnerThread())如果是的话,更新状态值nextc为c+acquires(setStates(sextc)),返回ture。

4.如果当前锁已经被获取,且获取线程不是当前线程,则返回false

这里的整体逻辑其实和我们自己构造的公平锁逻辑很相似了。主要的区别在于:我们为什么在这里把获取锁的状态值设置为了acquire,而不是简单的1?

其实道理很简单,设置为acquire之后,我们就可以根据状态值判断锁的重入次数。这样在释放的时候才不会释放错误。        


非公平锁:

非公平锁的代码逻辑几乎和公平锁一样,只不过在判断当前锁没有线程获取的时候,我们不再调用 hasQueuedPredecessors()方法来检测当前线程是否需要排队。也就是自由竞争。

让我们回到加锁的最外层: 

如何把一个线程加入到等待队列中也是热门面试题,因此我们在这里也顺带讲一下:

在ReentrantLock的加锁外层中,为了把一个线程放到等待队列中,我们使用了addWaiter方法:

addWaiter是AQS提供的代码:

整体的代码逻辑也很简单:

先接收传递过来的节点,再用一个for构成的死循环抱住核心逻辑代码。这样是确保进入该方法的线程都可以被添加到队列当中。因为for死循环了。

让我们解读一下源代码:

1.先获取到同步队列的尾节点,之后对尾节点进行非空判断,如果为空,就先初始化队列。

2.如果尾节点不为空,先设置待加入节点的前驱节点为尾节点。

3.使用cas操作,使得tail更新为node。(tail为始终指向尾节点的一个变量

4.将实际尾节点的next指针指向node。

5.返回node。

这样我们就完成了一次添加尾节点。其实就是一个很简单的尾插法。而我们要讲一下:为什么在更新Tail变量的时候,要使用cas操作?

这是因为在多线程竞争的条件下,我们要保证始终只有一个节点获取到tail,成为真正的尾节点,如果不使用cas算法来对tail进行维护的话,那么多个线程同时获取到tail之后,就会引发线程竞争

目前我们已经知道了addwaiter的作用就是把线程加入到阻塞队列中,那么下一步很明显就应该是阻塞在队列中的线程了,但是我们想一想逻辑:阻塞队列是先进先出的,那么在阻塞和唤醒的时候,就应该是:当前线程自己阻塞后,由前一个线程唤醒。由图可看:

而我们的新加节点也不是直接阻塞的,他需要告知前一个节点,而node节点 中就为我们维护了这样一个变量:

某一个node对象的waitStatus的值为0的时候,意味着他在释放锁之后不需要unpark其他对象,反之如果它的值为-1的话,就需要在释放锁之后unpark其他对象。 

那么很显然,这段代码逻辑应该就是在acquireQueued这个方法中执行的;

 让我们来看一看这个方法:

这段方法的逻辑为:先获取到这个节点的前一个节点:如果前一个节点为头节点的话,说明当前线程是一个进行排队的,那么就调用tryAcquire方法进行加锁。

如果加锁成功,就调用setHead方法,把线程变为头节点。

这种方式使得加锁成功后,线程不会再留到这个队列当中。

如果不是头节点,就调用shouldParkAfterFailedAcquire()方法:

这个其实就是我们之前讲到的设置waitStatus的逻辑。 

我们分别来看这几个代码的逻辑:

1.如果前驱节点的waitStatus等于-1。就返回ture。

2.如果前驱节点的waitStatus>0.则说明此前驱节点不可用,我们就一直往前找,直到找到前驱节点小于等于0的节点。 

3.如果前驱节点的waitStatus=0,就使用CAS操作,将其值更新为-1。

我们来看看外围方法:

 也就是说:当shouldParkAfterFailedAcquire方法返回ture(前驱节点的waitstatus=-1)的时候,就调用parkandCheckInterrupt方法

解锁:

这是最外层的方法:基本逻辑为:

先调用tryRelease(arg)来尝试释放锁,如果锁释放成功,则肇东当前头节点,如果头节点不为空且waitstatus的值不为0,则证明有节点需要头节点unpark,那么就调用unparkSuccessor

否则tryRelease(arg)返回false,标识锁释放失败。

因此我们来看一看tryRelease方法:

整体的逻辑为:先用getstate-release(锁支持重入)。之后判断当前线程是否是持有锁的线程。如果不是就抛出异常。之后创建锁的标志量free,false标识释放失败,true标识释放成功。那么当c==0的时候,意味着锁释放完全了,就更新标识量free为true。并且同步设置锁的拥有者为null。再最外层更新锁的状态量state

这就是解锁的全部过程。

总结:

ReentrantLock 是 Java 并发包中的一个重要类,它提供了一种可重入的互斥锁实现。在多线程环境下,ReentrantLock 提供了更灵活和强大的同步控制机制,相比于 synchronized 关键字,它具有更多的特性和扩展能力。

首先,ReentrantLock 采用了独占模式,即同一时刻只允许一个线程持有锁。这保证了被锁保护的临界区只能被一个线程访问,从而避免了多个线程同时修改共享资源导致的数据竞争和不一致性。

其次,ReentrantLock 支持可重入,也就是说同一个线程可以多次获取同一个锁。这种特性使得线程可以在持有锁的情况下重复进入同一个临界区,并且在退出时释放锁。这样的设计简化了编程模型,并且避免了死锁的可能。

此外,ReentrantLock 还提供了公平性选择的机制。通过构造函数传入的参数可以选择公平模式或非公平模式,默认是非公平模式。在公平模式下,锁会按照请求的顺序分配给等待线程,避免了线程饥饿的情况。在非公平模式下,锁的获取是无序的,可能会导致某些线程一直获取不到锁。

如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!

 

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

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

相关文章

【机器学习】调配师:咖啡的完美预测

有一天,小明带着一脸期待找到了你这位数据分析大师。他掏出手机,屏幕上展示着一份详尽的Excel表格。“看,这是我咖啡店过去一年的数据。”他滑动着屏幕,“每个月的销售量、广告投入,还有当月的气温,我都记录…

day16 DOM(2)——获取设置表单的值

目录 操作表单元素属性H5自定义属性——data 操作表单元素属性 表单很多情况,也需要修改属性,比如点击眼睛,可以看到密码,本质是把表单类型转换成文本框正常的有属性有取值的,跟其它的标签属性没有区别 获取&#xff…

web蓝桥杯真题--11、蓝桥知识网

介绍 蓝桥为了帮助大家学习,开发了一个知识汇总网站,现在想设计一个简单美观的首页。本题请根据要求来完成一个首页布局。 准备 开始答题前,需要先打开本题的项目代码文件夹,目录结构如下: ├── css │ └──…

开源进程/任务管理服务Meproc使用之HTTP API

本文讲述如何使用开源进程/任务管理服务Meproc的HTTP API管理整个服务。 Meproc所提供的全部 API 的 URL 都是相同的。 http://ip:port/proc例如 http://127.0.0.1:8606/proc在下面的小节中,我们使用curl命令向您展示 API 的方法、参数和请求正文。 启动任务 …

C++I/O流——(4)格式化输入/输出(第二节)

归纳编程学习的感悟, 记录奋斗路上的点滴, 希望能帮到一样刻苦的你! 如有不足欢迎指正! 共同学习交流! 🌎欢迎各位→点赞 👍 收藏⭐ 留言​📝 含泪播种的人一定能含笑收获&#xff…

【前端设计】流光按钮

欢迎来到前端设计专栏,本专栏收藏了一些好看且实用的前端作品,使用简单的html、css语法打造创意有趣的作品,为网站加入更多高级创意的元素。 css body{height: 100vh;display: flex;justify-content: center;align-items: center;background…

使用 crypto-js 进行 AES 加解密操作

在前端开发中,数据的加密和解密是为了保障用户隐私和数据的安全性而常见的任务。AES(Advanced Encryption Standard)是一种对称密钥加密算法,被广泛用于保护敏感信息的传输和存储。本文将介绍 AES 加解密的基本原理,并…

智慧文旅运营综合平台:重塑文化旅游产业的新引擎

目录 一、建设意义 二、包含内容 三、功能架构 四、典型案例 五、智慧文旅全套解决方案 - 210份下载 在数字化浪潮席卷全球的今天,智慧文旅运营综合平台作为文化旅游产业与信息技术深度融合的产物,正逐渐显现出其强大的生命力和广阔的发展前景。 该…

二叉树的直径(LeetCode 543)

文章目录 1.问题描述2.难度等级3.热门指数4.解题思路参考文献 1.问题描述 给你一棵二叉树的根节点,返回该树的直径 。 二叉树的 直径 是指树中任意两个节点之间最长路径的长度 。这条路径可能经过也可能不经过根节点 root 。 两节点之间路径的长度由它们之间边数…

【Docker】实战多阶段构建 Laravel 镜像

作者主页: 正函数的个人主页 文章收录专栏: Docker 欢迎大家点赞 👍 收藏 ⭐ 加关注哦! 本节适用于 PHP 开发者阅读。Laravel 基于 8.x 版本,各个版本的文件结构可能会有差异,请根据实际自行修改。 准备 新…

cs231n assignment1——SVM

整体思路 加载CIFAR-10数据集并展示部分数据数据图像归一化,减去均值(也可以再除以方差)svm_loss_naive和svm_loss_vectorized计算hinge损失,用拉格朗日法列hinge损失函数利用随机梯度下降法优化SVM在训练集和验证集计算准确率&a…

【图解数据结构】顺序表实战指南:手把手教你详细实现(超详细解析)

🌈个人主页:聆风吟 🔥系列专栏:图解数据结构、算法模板 🔖少年有梦不应止于心动,更要付诸行动。 文章目录 一. ⛳️线性表1.1 🔔线性表的定义1.2 🔔线性表的存储结构 二. ⛳️顺序表…

web前端项目-金山打字游戏【附源码】

金山打字 【金山打字】这是一个练习打字的游戏,当游戏开始后,界面从顶部不断落下内容为随机字母的方块,当按下相对应的按键时,就会清除对应方块。游戏难度会随着落下的速度加快而提高。玩家每次成功清除字母方块,都会…

spawn_group | spawn_group_template | linked_respawn

字段介绍 spawn_group | spawn_group_template 用来记录与脚本事件或boss战斗有关的 creatures | gameobjects 的刷新数据linked_respawn 用来将 creatures | gameobjects 和 boss 联系起来,这样如果你杀死boss, creatures | gameobjects 在副本重置之前…

六、数组(1)一维数组

所谓数组,就是一个集合,里面存放了相同类型的数据元素 特点1:数组中每个数据元素都是相同的数据类型 特点2:数组是由连续的内存位置组成的 一、一维数组的定义方式 1、数据类型 数组名[数组长度]; 2、数据类型 数组名[数组长度…

【JavaEE】_基于UDP实现网络通信

目录 1. 服务器 1.1 实现逻辑 1.2 代码 1.3 部分代码解释 2. 客户端 2.1 实现逻辑 2.2 代码 2.3 客户端部分代码解释 3. 程序运行结果 4. 服务器客户端交互逻辑 此篇内容为实现UDP版本的回显服务器echo server; 普通服务器:收到请求&#xff…

《WebKit 技术内幕》之五(3): HTML解释器和DOM 模型

3 DOM的事件机制 基于 WebKit 的浏览器事件处理过程:首先检测事件发生处的元素有无监听者,如果网页的相关节点注册了事件的监听者则浏览器会将事件派发给 WebKit 内核来处理。另外浏览器可能也需要处理这样的事件(浏览器对于有些事件必须响应…

【Linux】nc 网络诊断 | 文件传输 命令详解

目录 一、命令简介 二、命令使用 2.1 测试服务器 2.2 端口连通性测试 2.2.1tcp端口连通性测试 2.2.2udp端口连通性测试 2.3 文件及目录的传输 2.3.1 文件传输(TCP端口) 2.3.2 文件传输(UDP端口) 相关文章: 【网络】抓包工具Wireshark下载安装和基本使用教…

力扣343. 整数拆分(动态规划)

Problem: 343. 整数拆分 文章目录 题目描述思路解题方法复杂度Code 题目描述 思路 该题目可以抽象成动态规划中的爬楼梯模型,将整数的拆分类比为上台阶: 1.每个阶段可以从整数中划分出1、2、…k的一个整数 2.int dp[n 1] dp[i]表示为i的整数划分的最大…

怎么提升搜狗网站排名

在当今数字化时代,网站排名对于品牌、企业以及个人都至关重要。而对于许多网站来说,搜狗搜索引擎是一个重要的流量来源。为了在搜狗上取得更好的排名,不仅需要优化网站内容,还需要巧妙运用一些工具和技巧。在本文中,我…