ReentrantLock acquire方法源码解析

2024-01-29 02:48

本文主要是介绍ReentrantLock acquire方法源码解析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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

acquire方法首先判断获取锁是否成功,也就是tryAcquire方法的作用,以NonfairSync类中方法为例,下面是它的源码

        final boolean nonfairTryAcquire(int acquires) {//获取当前线程final Thread current = Thread.currentThread();//获取当前state的值,该值用来标识锁的情况,0表示未有线程持有,>0表示锁重入的次数int c = getState();(1)if (c == 0) {(2)if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}(3)else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}

(1)如果state的值为0,也就是锁还未有线程获取的情况;

(2)尝试获取锁,CAS操作将state由0置为1,如果成功,就将exclusiveOwnerThread属性置为当前线程,该属性用来标识哪个线程获取了锁;

(3)如果c不等于0,也就是有线程获取了锁,判断获取锁的线程是不是当前线程,如果是,就将重入state的次数加1,中间的if是溢出判断;

如果已经有线程获取锁或是在争抢锁时失败,就返回false;

在未获取到锁的情况下,会执行判断逻辑的后半部分acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

首先来看addWaiter方法,该方法的作用是在AQS队列中增加一个当前线程的等待节点,下面是它的源码:

    private Node addWaiter(Node mode) {(1)Node node = new Node(Thread.currentThread(), mode);//获取AQS队列的尾节点Node pred = tail;//判断尾节点是否为nullif (pred != null) {(2)node.prev = pred;if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}(3)enq(node);return node;}

(1)创建一个Node节点,其中节点中thread属性是当前线程,mode是null值,也就是下一个等待节点为null

        Node(Thread thread, Node mode) {     // Used by addWaiterthis.nextWaiter = mode;this.thread = thread;}

(2)如果尾节点不为null,将新节点的prev指向尾节点,并CAS操作将新节点置为尾节点,如果成功就设置尾节点的next节点为新节点,AQS队列是一个双向队列,并返回新节点。

(3)如果尾节点为null或在CAS置尾节点失败的情况下,执行enq方法

    private Node enq(final Node node) {for (;;) {Node t = tail;if (t == null) { // Must initializeif (compareAndSetHead(new Node()))tail = head;} else {node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}}

首先,如果tail为null,也就是AQS队列还未null的情况下,创建一个新节点,并CAS将其置为头节点,然后tail和head指向同一个节点,为什么这里需要CAS呢?因为,有可能出现两个线程同时在addWaiter中判断tail为null,然后都执行enq方法。

在初始化AQS队列后,还没有结束,for是一个死循环,等待的新节点还没有添加到AQS队列中,else就是添加节点的逻辑;

总的来讲,addWaiter的逻辑就是添加等待节点到AQS队列,如果队列还不存在,就创建一个哨兵节点,同时tail和head都指向它。

在添加完节点后。会执行acquireQueued方法,该方法比较复杂,下面重点来分析

    final boolean acquireQueued(final Node node, int arg) {//用来标识是否需要取消获取锁boolean failed = true;try {//标记当前线程是否需要中断boolean interrupted = false;for (;;) {(1)final Node p = node.predecessor();(2)if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return interrupted;}(3)if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}}

(1)获取node节点的前一个节点,赋值给p。

(2)如果p是head节点,就再次尝试获取锁,这里再次尝试获取锁的原因是如果有线程在前一次获取锁和这一次获取锁之间释放了锁,那么这里就有可能获取成功;

如果node节点的前一个节点是head节点,并且成功获取到了锁,就设置node节点为head,删除本来的头节点,并设置取消获取锁的标记为false,返回中断标记。

(3)如果node不是AQS中的第一个等待节点,或者获取锁失败,就执行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;}

创建节点的waitStatus都是0,所以一般在第一次执行该方法时,会设置pred的waitStatus为-1,返回false,在下一次进入该方法时,会返回true;

parkAndCheckInterrupt方法会在shouldParkAfterFailedAcquire返回true后执行,它会将线程阻塞挂起,并清除中断标记

    private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();}

如果线程本身是中断的,那会执行interrupted = true,并在selfInterrupt()再次中断。

所以总结一下,只有AQS队列中的第一个等待节点会被唤醒执行,其他节点会进入阻塞状态直到轮到自己成会AQS队列的第一个节点(哨兵节点后的第一个节点)。

 

总结

acquire方法,会尝试获取锁,如果获取锁失败,就创建一个该线程的等待节点插入到AQS队列的尾部,并阻塞当前线程,locl.relase会unparkAQS队列中的第一个非哨兵节点,线程被唤醒后就会尝试再次获取锁,如果还是没有获取到就再次park阻塞,如果获取到了,就返回中断位并是否再次中断当前线程。

 

这篇关于ReentrantLock acquire方法源码解析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/655530

相关文章

SQL 外键Foreign Key全解析

《SQL外键ForeignKey全解析》外键是数据库表中的一列(或一组列),用于​​建立两个表之间的关联关系​​,外键的值必须匹配另一个表的主键(PrimaryKey)或唯一约束(UniqueCo... 目录1. 什么是外键?​​ ​​​​2. 外键的语法​​​​3. 外键的约束行为​​​​4. 多列外键​

Java进行日期解析与格式化的实现代码

《Java进行日期解析与格式化的实现代码》使用Java搭配ApacheCommonsLang3和Natty库,可以实现灵活高效的日期解析与格式化,本文将通过相关示例为大家讲讲具体的实践操作,需要的可以... 目录一、背景二、依赖介绍1. Apache Commons Lang32. Natty三、核心实现代

使用Python自动化生成PPT并结合LLM生成内容的代码解析

《使用Python自动化生成PPT并结合LLM生成内容的代码解析》PowerPoint是常用的文档工具,但手动设计和排版耗时耗力,本文将展示如何通过Python自动化提取PPT样式并生成新PPT,同时... 目录核心代码解析1. 提取 PPT 样式到 jsON关键步骤:代码片段:2. 应用 JSON 样式到

Maven 插件配置分层架构深度解析

《Maven插件配置分层架构深度解析》:本文主要介绍Maven插件配置分层架构深度解析,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录Maven 插件配置分层架构深度解析引言:当构建逻辑遇上复杂配置第一章 Maven插件配置的三重境界1.1 插件配置的拓扑

Oracle 通过 ROWID 批量更新表的方法

《Oracle通过ROWID批量更新表的方法》在Oracle数据库中,使用ROWID进行批量更新是一种高效的更新方法,因为它直接定位到物理行位置,避免了通过索引查找的开销,下面给大家介绍Orac... 目录oracle 通过 ROWID 批量更新表ROWID 基本概念性能优化建议性能UoTrFPH优化建议注

Pandas进行周期与时间戳转换的方法

《Pandas进行周期与时间戳转换的方法》本教程将深入讲解如何在pandas中使用to_period()和to_timestamp()方法,完成时间戳与周期之间的转换,并结合实际应用场景展示这些方法的... 目录to_period() 时间戳转周期基本操作应用示例to_timestamp() 周期转时间戳基

在 PyQt 加载 UI 三种常见方法

《在PyQt加载UI三种常见方法》在PyQt中,加载UI文件通常指的是使用QtDesigner设计的.ui文件,并将其转换为Python代码,以便在PyQt应用程序中使用,这篇文章给大家介绍在... 目录方法一:使用 uic 模块动态加载 (不推荐用于大型项目)方法二:将 UI 文件编译为 python 模

Python将字库文件打包成可执行文件的常见方法

《Python将字库文件打包成可执行文件的常见方法》在Python打包时,如果你想将字库文件一起打包成一个可执行文件,有几种常见的方法,具体取决于你使用的打包工具,下面就跟随小编一起了解下具体的实现方... 目录使用 PyInstaller基本方法 - 使用 --add-data 参数使用 spec 文件(

Python的pip在命令行无法使用问题的解决方法

《Python的pip在命令行无法使用问题的解决方法》PIP是通用的Python包管理工具,提供了对Python包的查找、下载、安装、卸载、更新等功能,安装诸如Pygame、Pymysql等Pyt... 目录前言一. pip是什么?二. 为什么无法使用?1. 当我们在命令行输入指令并回车时,一般主要是出现以

通过C#获取Excel单元格的数据类型的方法详解

《通过C#获取Excel单元格的数据类型的方法详解》在处理Excel文件时,了解单元格的数据类型有助于我们正确地解析和处理数据,本文将详细介绍如何使用FreeSpire.XLS来获取Excel单元格的... 目录引言环境配置6种常见数据类型C# 读取单元格数据类型引言在处理 Excel 文件时,了解单元格