【图解】一篇搞定ReentrantLock的加锁和解锁过程

文章目录

      • 1. 概述
      • 2. AbstractQueuedSynchronizer(AQS)
      • 3. 加锁
      • 4. 解锁
      • 5. 公平锁和非公平锁的区别

1. 概述

本文主要结合图片分析ReentrantLock加锁和解锁过程的源码,加锁和解锁的原理不清楚的读者可以好好看看。

2. AbstractQueuedSynchronizer(AQS)

在分析ReentrantLock加锁和解锁的过程之前,先来了解一下AQS,ReentrantLock就是基于AQS实现的。AQS其实就是维护了一个双向链表,主要的属性如下所示:

 Thread exclusiveOwnerThread(继承父类AbstractOwnableSynchronizer)持有锁的线程Node head 指向阻塞队列的队头Node tail 指向阻塞队列的队尾int state 当前锁的状态

在这里插入图片描述

Node对象是对Thread进行封装,主要的属性如下所示:

   Thread thread 记录当前结点关联的线程Node prev 指向在前驱结点Node next 指向后继结点int waitStatus 结点状态标记 在之后的博客中再来详细的介绍这个属性

下面先来分析ReentrantLock的加锁过程

3. 加锁

加锁对外提供的API有lock(),lockInterruptibly(),tryLock(),tryLick(long,TimeUnit),四种方法,本文主要分析lock()方法的过程,其他的大同小异,在之后的博客当中再更新这4者的使用区别和原理细节的不同。

lock方法如下,内部调用了AQS当中的lock方法

在这里插入图片描述

ReentrantLock当中AQS有两种实现,一种是公平锁FairSync,另一种是非公平锁NonFairSync。而默认的实现是非公平锁,下面来主要分析非公平锁的lock()方法。

在这里插入图片描述

这里比较简单,if判断 cas修改锁标记是否成功,若修改成功,那么就修改exclusiveOwnerThread属性为当前线程,否则调用acquire方法 继续抢锁。

例如此时t1线程当中调用了lock方法抢到了锁,此时的ReentrantLock 对象如下所示:
在这里插入图片描述

接着第二个线程 t2 调用了lock方法,那么if得到的结果肯定是false,修改状态标记失败,会调用acquire方法,下面来看看acquire方法的逻辑

在这里插入图片描述

这里涉及到了4个方法,一个一个按照逻辑调用的顺序来说明:

  1. tryAcquire 如果返回false —> 2. addWait —> 3. acquireQueued 返回true —> 4. selfInterrupt

先来分析tryAcquire

在这里插入图片描述

内部调用nonfairTryAcquire

在这里插入图片描述

该方法有两种情况可以拿到锁

  1. 锁没有人占用
  2. 持有锁的线程和当前线程相同,表示可重入

但是现在假设的当前线程是t2线程,此时锁是被t1持有的,所有这时候t2拿不到锁,那么tryAcquire的结果就是false

接下来会调用addWait方法

在这里插入图片描述

将当前t2线程,封装成node结点,pred指针指向tail

当前tail为空,所以不会进入if

调用enq方法,enq方法代码如下:

在这里插入图片描述

这里是一个死循环,第一轮循环新的指针t指向tail,此时tail为空,所以会进行cas 修改head结点(cas是原子性的就算此时多个线程都进入了enq方法,那么有且仅有一个设置头结点成功,其余的线程进入下一次for循环),然后将tail指向head,此时的ReentrantLock如下图所示:

在这里插入图片描述

第一层循环结束,该方法并没有结束,进行第二次的for循环,此时t不等于null,所以会进入else的逻辑

node结点的前驱指向t,cas修改tail指向node,修改t的next等于node,结果如下:在这里插入图片描述

所以addWaiter方法就是将当前线程封装成Node结点,然后放入阻塞队列的尾部,也就是入队操作。

接下来就来到第三个方法acquireQueued

在这里插入图片描述

这里主要是一个死循环,首先拿到当前结点的前驱结点p,对于现在的例子来说,当前结点是t2的结点,它的前驱就是head。此时p等于head 可以再一次的调用tryAcquire去获取锁。

为什么这里还要再来一次呢?是因为再上一次获取锁失败 到t2 形成Node结点入队的这个期间,t1可能会释放锁。这样t2就可以在这里获取到锁了。可以说这样做是为了减少线程阻塞唤醒的次数。

如果t1 释放了,那么t2就有可能可以获得到锁。此时就调用setHead方法修改head指针,并把当前结点的thread 设置为null,原先head结点的后继设置为空。这样原先head结点 就没有和GCroot关联,在垃圾回收的时候就会处理掉它。ReentrantLock如下图所示:

在这里插入图片描述

如果t1此时没有释放锁,那么t2 再次获取锁失败。则会调用shouldParkAfterFailedAcquire方法判断是否需要park,代码如下:

在这里插入图片描述

获取前驱结点的ws,如果ws等于-1直接返回true,表示需要park。

否则走下面的逻辑,如果ws大于0,说明该线程取消了,那么就进行循环,将取消的线程跳过,什么是取消的线程之后再讨论,一般情况下是不会进入这个if的。如果ws不大于0 走else的逻辑,cas设置前驱结点的ws为-1。设置为-1的意义表示,当前ws为-1的结点有义务唤醒它的后继结点。在之后的锁的释放当中,可以看到ws的作用。

此时ReentrantLock的结果如下:

在这里插入图片描述

上面的shouldParkAfterFailedAcquire逻辑执行完 返回false。那么它就不会调用parkAndCheckInterrupt。继续执行下一个for循环。

同样的此时t2结点的前驱还是head,所以可以再一次的尝试获取锁。看看此时t1有没有释放锁,如果释放锁了,那么就可以拿到,否则继续调用shouldParkAfterFailedAcquire。但是此时前驱结点的ws已经是-1了 所以直接返回true。

接着就会执行parkAndCheckInterrupt方法,这才阻塞了当前线程。代码如下:

在这里插入图片描述

可以看到ReentrantLock的加锁过程,尽可能的让当前线程更多机会的去尝试获取锁,避免线程阻塞,发生系统调用。

假设此时t3线程调用了lock方法,相信读者可以分析出此时ReentrantLock的结果了吧,如下所示:在这里插入图片描述

4. 解锁

解锁对外提供的API是unlock方法,内部调用AQS的release方法,代码如下:

在这里插入图片描述

release方法如下:

在这里插入图片描述

调用tryRelease 当前线程释放锁。

如果释放成功 则获取头结点,如果头结点的ws !=0 也就是等于-1的话,会调用unparkSuccessor来唤醒下一个结点。

下面就来分析tryReleaseunparkSuccessor方法

tryRelease的代码如下:
在这里插入图片描述

计算当前锁的新的state状态 =》c

判断当前线程是否是持有锁的线程,如果不是的话直接抛出异常,没有持有锁就想释放锁 不是做梦吗。。。

设置锁是否完全释放的标记free

如果c等于0 说明锁被完全释放了,那么将完全释放标记free设为true,再将AQS中的exclusiveOwnerThread设置为null,表示没有线程持有这把锁。

更新state,返回free。true 表示完全释放了锁,false 表示还没有完全释放锁。

执行完tryRelease,ReentrantLock对象如下图所示:
在这里插入图片描述

如果该方法返回true 接着会调用unparkSuccessor方法去唤醒阻塞队列当中的下一个结点。

在这里插入图片描述

此处的node是头结点

首先将node的ws修改为0,然后拿到头结点的后继结点。如果后继结点为空,或者后继结点关联的线程被取消了,那么就从tail开始往前找到ws小于0的最靠前的结点赋值给s。

然后将s结点的线程唤醒。一般情况下就是唤醒阻塞队列当中的头结点的后继结点。

unparkSuccessor方法结束后,ReentrantLock对象如下图所示:

在这里插入图片描述

t2被唤醒,回想一下刚才t2 是在哪里被阻塞的,是在parkAndCheckInterrupt 当中调用了LockSupport.park(this)阻塞的。那么t2被唤醒后,会继续从这里开始执行后面的代码

在这里插入图片描述

之后的代码 调用了Thread.interrupted()。清除打断标记,至于为什么要这样做之后再说明。

再调用Thread.interrupted()方法之前,如果当前线程被打断过,清除打断标记 返回true,如果没有被打断过则返回false。

此时t2没有被打断过,所以返回false。函数返回上一层就来到了acquireQueued方法当中,继续执行

在这里插入图片描述

因为parkAndCheckInterrupt()返回false,所以此处是否被中断的标记并没有被修改,继续执行下一次for循环。

拿到t2结点的前一个结点p,此时p就是head 所以会调用tryAcquire尝试获得锁。tryAcquire方法前面已经分析过了,此时如果没有其他线程来抢锁,那么t2肯定是可以拿到锁的,返回true。

然后更新AQS的队头,修改p结点的后继 方便垃圾回收,返回中断标记。acquireQueued方法结束,ReentrantLock对象如下所示:

在这里插入图片描述

至于此处的failed标记有什么用?

是为了在JVM运行抛出异常或者程序员调用API错误导致了一些异常的情况下,在finally块中执行cancelAcquire方法取消当前线程获取锁的操作。被取消的线程对应的Node结点当中的ws被设置为1.所以之前唤醒后继结点的时候,会将ws大于0的结点跳过,因为它已经取消获取锁的操作了。

还记得acquire方法当中有4个方法吗,再来回顾一下

在这里插入图片描述

  1. tryAcquire 如果返回false —> 2. addWait —> 3. acquireQueued 返回true —> selfInterrupt

前面3个方法已经分析过了,还剩下最后一个。这里也就会涉及到为什么被唤醒的线程要调用Thread.interrupted()清除中断标记。当parkAndCheckInterrupt()方法返回true的时候,会将打断标记设置为true。也就是说,在这个线程被打断后程序需要对它进行一定的处理,但是这里又无法像sleep一样响应中断,所以Doug Lea的响应中断的逻辑就是 让它成为没有被打断过一样,继续的执行代码,拿到锁之后返回中断标记。如果此时的中断标记为true,那么就会执行上面的selfInterrupt方法,进行补偿中断,中断当前线程,设置中断标记为true,改回来它曾经被中断过。之后程序员如果需要这个中断标记做一些其他的业务处理的话,就不会受到影响。

在这里插入图片描述

selfInterrupt代码比较简单,就是进行了一次打断,设置中断标记为true

在这里插入图片描述

以上就是非公平锁的加锁和释放的过程。

5. 公平锁和非公平锁的区别

先来看看两个lock方法代码,区别还是挺明显的,非公平锁,会先进行cas尝试拿锁,如果拿锁失败了才会调用acquire,而公平锁是直接调用acquire方法

在这里插入图片描述

acquire方法有什么区别呢?看下面的代码
在这里插入图片描述

这里是一样的,区别在于tryAcquire当中的实现不一样,下图左边是非公平锁的实现,右图是公平锁的实现,主要区别就一行代码,是否调用hasQueuedPredecessors方法,当hasQueuedPredecessors的返回结果是false的时候,公平锁才会调用cas尝试获取锁

在这里插入图片描述

下面来分析hasQueuedPredecessors方法什么情况下会返回false

在这里插入图片描述

两种情况会返回false

  • 队列为空。h==t 这时候说明没有其他线程在等待拿锁,此时可以去尝试拿锁
  • 队列不为空,但是当前头结点的后继结点关联的线程等于当前线程,此时表示要进行重入拿锁。

总结区别:

  • 使用非公平锁,线程来尝试拿锁的时候,不用先看阻塞队列当中是否有线程在等待拿锁,都可以先进行尝试拿锁,没拿到才进入队列当中。
  • 使用公平锁,线程需要看阻塞队列当中是否有线程在等待,如果有线程在等待的话,如果头结点的后继结点关联的线程不是当前线程,那么就没法加锁,直接进入队列

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

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

相关文章

Lock锁和ReentrantLock锁

前言 JDK 1.5中提供的锁的接口java.util.concurrent.locks.Lock,其提供了一些ReentrantLock, ReentrantReadWriteLock实现类。 参考JDK文档:Java Platform SE 6 目录 前言 Lock接口 ReentrantLock 公平性和非公平性 公平锁与非公平锁的使用示例 A…

ReentrantLock源码分析(一)加锁流程分析

一、ReetrantLock的使用示例 static ReentrantLock lock new ReentrantLock(); public static void main(String[] args) throws InterruptedException { new Thread(ClassLayOutTest::reentrantLockDemo, "threadA").start(); Thread.sleep(1000);…

Lock与ReentrantLock

Lock Lock位于java.util.concurrent.locks包下,是一种线程同步机制,就像synchronized块一样。但是,Lock比synchronized块更灵活、更复杂。 1. Lock继承关系 2. 官方文档解读 3. Lock接口方法解析 public interface Lock {// 获取锁。如果锁…

ReentrantLock介绍

文章目录 ReentrantLock1、构造函数2、公平性锁和非公平性锁(1)公平性锁和非公平性锁示例(2)公平锁和非公平锁的实现公平性锁:FairLock非公平性锁 (3)Condition生产者和消费者 循环打印ABC Reen…

ReentrantLock锁相关方法

目录 Lock接口的实现类 ReentrantLock的方法 ReentrantLockTest测试 用于测试的线程 t1测试 正确释放重入锁 获取当前的重入次数 t1t2测试 使用islocked()方法检测锁状态 t1t3测试 使用trylock方法尝试获取锁 使用isHeldByCurrentThread方法检测当前线程是否持有锁 不…

ReentrantLock详解

目录 一、ReentrantLock的含义 二、RerntrantLock当中的常用方法 ①lock()和unlock()方法 ②构造方法 ③tryLock()方法 tryLock()无参数 tryLock(timeout,Times)有参数 ④lockInterruptibly() throws InterruotedException 经典面试问题: ReentrantLock和synchronized有什…

OpenAI最新官方ChatGPT聊天插件接口《智能聊天插件引言》全网最详细中英文实用指南和教程,助你零基础快速轻松掌握全新技术(一)(附源码)

Chat Plugins Limited Alpha 聊天插件 前言IntroductionPlugin flow 插件流其它资料下载 Learn how to build a plugin that allows ChatGPT to intelligently call your API. 了解如何构建允许ChatGPT智能调用API的插件。 前言 在现代的软件开发环境中,使用第三方…

Pycharm快速入门(10) — 插件管理

1、插件安装 File | Settings | Plugins | Marketplace 搜索插件点击Install安装 2、插件卸载 File | Settings | Plugins | Installed 选择需要卸载的插件,点击Uninstall。 3、推荐插件 (1)、Chinese ​(Simplified)​ Language Pack &am…

chatgpt赋能python:Python编程的好玩之处:用简单的代码创造奇妙的世界

Python编程的好玩之处:用简单的代码创造奇妙的世界 如果你喜欢写代码,那么Python是一个不错的选择。Python语言设计简单,易学易用,同时还拥有丰富的生态系统,支持许多强大的第三方库和框架,可以使你轻松地…

chatgpt赋能python:Python图片拼图的好处和应用

Python图片拼图的好处和应用 Python是一种高级编程语言,已经被广泛应用于数据科学、网络编程、机器学习等领域。其中,Python的图像处理领域也越来越受关注。在本文中,我们将介绍如何使用Python创建图片拼图,并讨论它的好处和应用…

midjourney教程:如何快速生成个性化Logo设计

midjourney是一款基于人工智能技术的Logo设计工具,它可以帮助用户快速生成个性化的Logo设计,而无需具备专业的设计技能。下面将为大家介绍midjourney的使用方法,以帮助大家轻松生成符合自己需求的Logo设计。 第一步:登录midjourn…

chatgpt赋能python:Python添加图片背景的方法

Python添加图片背景的方法 简介 Python是一种开源的高级编程语言,广泛应用于各个行业中,包括图像处理。添加图片背景是图像处理中的常见需求,通过Python可以很方便地实现。 本篇文章将介绍如何使用Python来给图片添加背景,让您…

chatgpt赋能python:Python怎么做图形

Python怎么做图形 在数据可视化和图像处理方面,Python已经成为了最受欢迎的编程语言之一。Python的图形库使得创建各种图形和图表、可视化工具和图像处理应用程序变得容易而简单。 在本文中,我们将会介绍一些最受欢迎的Python图形库,以帮助…

程序员晒追女神聊天截图,坦言第一次没经验,网友直呼凭实力单身

前段时间网络上一名程序员晒出了自己与女神之间的聊天记录的对话截图,通过截图中我们可以看出,应该是这位程序员在追求这位女神,但是短短的十几分钟几条聊天记录,却以女神不再愿意搭理程序员结束,对于这样的结局&#…

程序员给女友4千生活费,收到女友错发信息后分手,神对话!

如何平衡好亲情爱情的关系,是一门学问,有的人就希望自己的另一半过好他们自己的小日子,不要对家里的事情过多的付出,但有人觉得自己父母养大自己不容易,能有能力的话,不光孝敬爹妈,还会帮衬家里…

程序员就是这样聊天把女朋友聊没的

身为程序员 都想当然的认为 身为一个优秀的程序员 我怎么可能会没女票 这不科学啊 工资高,话少 有一天看到了 某个程序员的聊天记录 有女孩主动搭讪 这么绝好的机会 然后你竟然说忙 说忙 忙... 主动找你搭讪 你还不抓紧机会约约约 如果改成&#xff1a…

被程序员的相亲规划整不会了......

近日,北京一程序员将自己7天7场相亲行程规划表发到论坛分享,感叹到:真不是凡尔赛,相亲比上班还难,引来大量网友围观。 相亲也有规划表? 据介绍,该程序员今年刚好30岁,自己平时加班多…

程序员吐槽女朋友狮子大开口

本文转载自程序员八卦 一个程序员发帖吐槽自己的潮汕女朋友,开口要彩礼18万8,楼主在网上查了一下,一般潮汕彩礼是3万到8万,难道外地人要多给一点吗?而且女朋友还一定要楼主父母出彩礼,不能楼主自己出&…

最最普通程序员,如何利用工资攒够彩礼,成为人生赢家

今天我们不讲如何提升你的专业技能去涨工资,不讲面试技巧如何跳槽涨工资,不讲如何干兼职赚人生第一桶金,就讲一个最最普通的程序员,如何在工作几年后,可以攒够彩礼钱,婚礼酒席钱,在自己人生大事…

如何做好小红书?从找好定位开始,这篇文章告诉你

近年来小红书随着用户体量壮大和平台多元化发展,用户的兴趣点,早已从美妆独大变为渗透生活领域的各个方面。与以往相比,大家对小红书的认知也逐渐在发生变化。 如果说去年还有不少商家还经常问我们“为什么要做小红书?”。那么&am…