图解java.util.concurrent并发包源码系列——深入理解ReentrantLock,看完可以吊打面试官

图解java.util.concurrent并发包源码系列——深入理解ReentrantLock,看完可以吊打面试官

  • ReentrantLock是什么,有什么作用
  • ReentrantLock的使用
  • ReentrantLock源码解析
    • ReentrantLock#lock方法
      • FairSync#tryAcquire方法
      • NonfairSync#tryAcquire方法
    • ReentrantLock#unlock方法
  • ReentrantLock的其他方法简介
    • ReentrantLock#lockInterruptibly
    • ReentrantLock#tryLock
    • ReentrantLock#tryLock(带参数)
    • ReentrantLock#newCondition

往期文章:

  • 人人都能看懂的图解java.util.concurrent并发包源码系列 ThreadPoolExecutor线程池
  • 图解java.util.concurrent并发包源码系列,原子类、CAS、AtomicLong、AtomicStampedReference一套带走
  • 图解java.util.concurrent并发包源码系列——LongAdder
  • 图解java.util.concurrent并发包源码系列——深入理解AQS,看完可以吊打面试官

上一篇文章(图解java.util.concurrent并发包源码系列——深入理解AQS,看完可以吊打面试官 )介绍了AQS的原理和源码,然后使用AQS实现了一个排他锁,这次我们看看Java的并发包里面基于AQS实现的锁工具类——ReentrantLock。本文会介绍ReentrantLock的功能,以及ReentrantLock如何通过AQS实现它的功能,但是不会再介绍AQS的相关原理,关于AQS的原理和源码,可以看上一篇文章。

ReentrantLock是什么,有什么作用

ReentrantLock是可重入锁,当我们有一些资源需要互斥访问,或者有某一块代码需要互斥执行时,可以使用ReentrantLock加锁,对互斥资源和互斥代码块起到一个保护作用,保证同一时刻只会有一个线程访问。

在这里插入图片描述

可重入的意思就是当一个线程获取到锁之后,如果它再次获取同一把锁,是可以获取成功的。而不可重入锁则是当前线程获取到锁以后,如果它再次获取,是获取不成功的,也就是自己把自己给阻塞住了,我们上一篇文章最后面的例子就是一个不可重入锁。

在这里插入图片描述

ReentrantLock的使用

ReentrantLock的用法非常简单,它提供了lock方法用于获取锁,unlock方法用于释放锁。

调用lock方法后,当前线程会去获取锁,获取不到锁会阻塞等待,直到获取到锁,该方法才会返回。如果重复调用多次lock方法,就重复加锁多次,当前线程不会阻塞(可重入),但是它需要多次释放锁。

调用unlock方法,当前线程就会释放锁,然后唤醒其他阻塞等待获取锁的线程。

我们还是用上一篇文章最后的那个例子,去观察ReentrantLock的作用。

    public static void main(String[] args) throws InterruptedException {int[] num = {0};// 创建一个ReentrantLock对象ReentrantLock reentrantLock = new ReentrantLock();Thread[] threads = new Thread[100];for (int i = 0; i < 100; i++) {Thread thread = new Thread(() -> {for (int j = 0; j < 10000; j++) {try {// 加锁reentrantLock.lock();num[0]++;} finally {// 释放锁,防止finally块中是因为防止抛异常导致锁得不到释放reentrantLock.unlock();}}});thread.start();threads[i] = thread;}for (int i = 0; i < threads.length; i++) {threads[i].join();}System.out.println(num[0]);}

在这里插入图片描述

可以看到输出结果是正确的。

用法跟我们上一篇文章最后面的例子几乎是一模一样的。首先要创建一个ReentrantLock对象,然在线程try代码块中调用ReentrantLock的lock方法进行加锁,在finally代码块中调用ReentrantLock的unlock方法进行解锁操作,解锁操作在finally块中是因为防止线程抛异常导致锁得不到释放。

ReentrantLock源码解析

接下来看看ReentrantLock的lock方法和unlock方法的内部源码,看看它是怎么实现锁的获取和释放的。

ReentrantLock#lock方法

ReentrantLock#lock

    public void lock() {sync.lock();}

可以看到和我们上一篇文章的例子一样,都是有一个内部类,这个内部类肯定继承了AQS。但是它这里没有直接调用AQS的acquire方法,而是调用了一个lock方法,我们进去lock方法看一看。

abstract static class Sync extends AbstractQueuedSynchronizer {private static final long serialVersionUID = -5179523762034025860L;abstract void lock();// ...省略了下面的代码
}

可以看到Sync继承了AQS,但是Sync本身还是一个抽象类,而lock方法也是一个抽象方法。

在这里插入图片描述

可以看到Sync有两个实现类,FairSync和NonfairSync。

在这里插入图片描述

也就是说ReentrantLock内部的sync有可能是FairSync,也有可能是NonfairSync。那什么时候是FairSync、什么时候是NonFairSync呢?我们看看ReentrantLock的构造方法就知道了。

    /*** 默认非公平锁*/public ReentrantLock() {sync = new NonfairSync();}/*** fair为true时,是公平锁,为false则是非公平锁*/public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}

ReentrantLock可以实现公平锁和非公平锁的功能,默认是非公平锁,如果我们调用有参构造方法,传递的fair参数是true就是公平锁,使用的sync就是FairSync,否则就是非公平锁,使用的就是NonfairSync。

在这里插入图片描述

公平锁就是当一个线程获取锁时,会先看一下队列中是否有线程等待获取锁,如果有,那么它会入队列。非公平锁则相反,如果一个线程要获取锁,那么不管三七二十一,先尝试获取一下,如果获取成功,则不用进队列。非公平锁的效率是比公平锁高的,所有默认的就是非公平锁。

我看一下lock方法的具体实现。

    static final class FairSync extends Sync {private static final long serialVersionUID = -3000897897090466540L;final void lock() {acquire(1);}// ......省略下面的代码}

FairSync的lock方法直接调用了AQS的acquire方法,与我们上一篇文章的例子一致。

    static final class NonfairSync extends Sync {private static final long serialVersionUID = 7316153563782823691L;final void lock() {if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);}// ......省略下面的代码}

NonfairSync的lock方法,会使用AQS的compareAndSetState方法尝试获取锁,如果CAS成功那么就不需要走acquire方法的逻辑了,如果CAS失败,才会调用AQS的acquire方法。

在这里插入图片描述

FairSync#tryAcquire方法

FairSync的lock方法会调用AQS的acquire方法,acquire方法会调用tryAcquire方法,然后会进入到FairSync的tryAcquire方法。

        protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState(); // 获取AQS中的state变量if (c == 0) { // state等于0表示锁没有被获取if (!hasQueuedPredecessors() && // 查看队列中是否有等待获取锁的线程compareAndSetState(0, acquires)) { // CAS尝试获取锁setExclusiveOwnerThread(current); // 获取锁成功,设置当前线程为占有锁的线程return true; // 返回true,表示告诉AQS获取锁成功}}else if (current == getExclusiveOwnerThread()) { // 锁已被获取,但是获取锁的线程是当前线程int nextc = c + acquires; // state加acquiresif (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc); // 设置state的值return true; // 返回true,表示告诉AQS获取锁成功}return false; // 返回true,表示告诉AQS获取锁不成功}}

FairSync的tryAcquire方法首先会调用AQS的getState方法获取state变量,这里是当state为0才表示锁没有被获取,与我们上一篇文章的例子不一样。

如果state为0,那么锁没有被获取,此时会调用hasQueuedPredecessors()方法检查队列中是否有等待获取锁的线程,如果有,那么当前线程不会获取锁,而是返回false直接进队列排队,如果队列中没有等待获取锁的线程,那么会调用AQS的compareAndSetState方法尝试修改state变量,修改成功表示获取锁成功,此时就会设置当前线程为占有锁的线程,然后方法返回true,表示获取锁成功。

如果state不为0,那么会检查当前线程是否是占有锁的线程,如果是,那么会执行重入的逻辑,然后方法返回true。如果当前线程不是占有锁的线程,那么方法返回false,表示获取锁失败。

在这里插入图片描述

下面看一下hasQueuedPredecessors方法是怎么判断队列中是否有线程的。

    public final boolean hasQueuedPredecessors() {Node t = tail; // 尾节点Node h = head; // 头节点Node s;return h != t &&((s = h.next) == null || s.thread != Thread.currentThread());}

h != t 判断头节点和尾节点是否不相等,不相等才有可能有线程在队列中,相等的话表示队列为空,那么肯定是没有线程在队列中的。

如果头节点和尾节点不相等了,那么看h.next头节点的后继节点是否为空,如果为空,那么就表示当前队列已经有等待获取锁的线程。那么为什么头尾节点不相等,并且头节点的next指针为空,就表示有线程已经入队列了呢?还有,什么情况下头节点指针和尾节点指针不相等,但是头节点的next指针为null呢?答案就在AQS初始化队列以及线程节点入队的方法中,也就是AQS的enq方法。

    private Node enq(final Node node) {for (;;) {Node t = tail;if (t == null) { // Must initializeif (compareAndSetHead(new Node())) // A:使用CAS的方式设置头节点指针指向一个空节点tail = head; // B:设置尾指针指向和头指针指向的同一节点} else {node.prev = t; // C:设置node的prev指针指向当前队列的尾节点if (compareAndSetTail(t, node)) { // D:使用CAS的方式设置尾节点指针指向当前nodet.next = node; // E:设置原来的尾节点的next指针指向当前nodereturn t;}}}}

可以看到AQS的enq方法,分成了5个步骤。注意!这里的前提是AQS的队列还未初始化!

假设现在AQS队列还未初始化,在当前线程获取锁之前,有一个线程执行到了enq方法,然后这个线程执行完A、B、C、D四步,也就是它成功初始化了AQS的队列,然后又用CAS的方式修改尾节点的指针指向它自己的node节点,但是就是没有执行代码E,此时是不是头节点指针和尾节点指针不相等(h != t),并且头节点的next指针是null((s = h.next) == null)?此时这个线程已经算是入了队列的,如果此时有其他线程来获取锁(公平锁),那么是应该要排队的。所以当前线程获取锁的时候,执行到hasQueuedPredecessors()方法,就会返回true,表示已经有线程在队列中等待获取锁了。

那么假设现在AQS队列还未初始化,如果在当前线程获取锁之前,有一个线程执行到了enq方法,然这个线程执行完A、B,没有执行后面三步呢?那么此时头节点指针和尾节点指针还是相等的(h != t 不成立),此时这个线程也不算入队列,所以此时如果有别的线程来调用hasQueuedPredecessors()方法,hasQueuedPredecessors()方法放回false(队列中没有等待获取锁的线程),也是正确的。

如果头节点指针和尾节点指针不相等(h != t 为true),然后头节点的next指针不为null((s = h.next) == null 为false),但是头节点的next指针指向的节点对应的线程等于当前线程(s.thread != Thread.currentThread() 为false),那么hasQueuedPredecessors()方法返回false,当前线程可以尝试获取锁,也是正确的,因为很明显这种情况就是我已经在排队了,并且此时轮到我了。如果头节点的next指针指向的节点对应的线程不是当前线程(s.thread != Thread.currentThread() 为true),hasQueuedPredecessors()方法返回true,那么当前线程就只能去排队。

在这里插入图片描述

FairSync的tryAcquire方法就介绍到这里。

在这里插入图片描述

NonfairSync#tryAcquire方法

NonfairSync的tryAcquire方法里面直接调用了nonfairTryAcquire方法。

        protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}

nonfairTryAcquire方法位于Sync抽象类内部。

        final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {// 锁没有被获取,尝试获取锁if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}// 锁已被获取,查看是否是当前线程获取的,如果是,那么执行重入逻辑else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}// 获取锁失败return false;}

进入到Sync的nonfairTryAcquire方法内部,如果判断锁没有被获取(state == 0)就直接先CAS一下,如果CAS成功,该方法就返回true,表示获取锁成功。如果锁已被获取,那么判断当前线程是否是持有锁的线程 (current == getExclusiveOwnerThread()),如果当前线程是持有锁的线程,那么执行锁重入的逻辑,然后方法返回true,表示获取锁成功。否则返回false,表示获取锁失败。

在这里插入图片描述

ReentrantLock的lock方法到这里就介绍完了。

在这里插入图片描述

ReentrantLock#unlock方法

接下来看一下ReentrantLock的unlock方法的具体逻辑。

    public void unlock() {sync.release(1);}

ReentrantLock的unlock方法调用了sync的release方法,这个和我们上一篇文章的例子是一样的。

AQS的release方法会调用tryRelease方法。因为释放锁的操作不需要区分公平还是非公平,都是一样的释放锁的逻辑,所以tryRelease方法就放到了Sync抽象类的内部。也就是说,FairSync和NonfairSync的tryRelease是同一个方法,都是Sync的tryRelease方法。

        protected final boolean tryRelease(int releases) {int c = getState() - releases;// 如果当前线程不是持有锁的线程,抛异常if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;// c == 0,表示锁被释放,free设为true表示锁被释放,设置持有锁的线程为nullif (c == 0) {free = true;setExclusiveOwnerThread(null);}// 更新statesetState(c);return free;}

Sync的tryRelease方法首先会调用AQS的getState方法获取state变量,然后计算state变量减去参数releases之后的值c。然后判断如果c等于0,表示锁被释放了,那么设置返回值free为true表示锁被释放,然后setExclusiveOwnerThread(null)设置持有锁的线程为null。然后不管c是否等于0,最后都会调用AQS的setState方法,更新state变量,最后返回free。

如果返回的free为true,表示锁被释放了,如果返回的free为false,表示锁还没有被释放。比如当前线程重复获取锁5次,现在调用了unlock方法3次,那么state等于2,锁还没有被释放,它需要再调用两次,state才等于0,此时锁才被真正释放。

在这里插入图片描述

到这里ReentrantLock的unlock方法也介绍完了。

ReentrantLock的其他方法简介

ReentrantLock不仅仅只有lock和unlock两个方法,还有其他获取和释放锁的方法,下面做一个简单介绍,不做深入的描述。

ReentrantLock#lockInterruptibly

    public void lockInterruptibly() throws InterruptedException {sync.acquireInterruptibly(1);}

ReentrantLock的lockInterruptibly是以响应中断的方式获取锁,就是说在获取锁的过程中如果其他线程调用了thread.interrupt()方法打断了当前线程,那么当前线程会响应中断,不再继续获取锁。而我们上面介绍的ReentrantLock#lock方法,是不响应中断的,也就是说其他线程调用了当前线程的thread.interrupt()方法,当前线程也不做任何响应。

ReentrantLock#tryLock

    public boolean tryLock() {return sync.nonfairTryAcquire(1);}

ReentrantLock的tryLock方法是尝试获取锁,也就是尝试获取一下,成就成,不成就走,也不会阻塞。

ReentrantLock#tryLock(带参数)

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

ReentrantLock还有一个带参数的tryLock方法 boolean tryLock(long timeout, TimeUnit unit),该方法和无参的tryLock方法一样,也是尝试获取锁,成就成,不成不会马上走,而是阻塞等待一段时间。timeout参数就是指定阻塞等待的时间,unit则是时间单位。

ReentrantLock#newCondition

    public Condition newCondition() {return sync.newCondition();}

ReentrantLock的newCondition可以返回一个Condition对象,Condition 可以实现类似于和synchronized加Object#wait的功能,这个我们后面的文章再介绍吧。

在这里插入图片描述

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

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

相关文章

微信小程序iconfont真机渲染失败

解决方法&#xff1a; 1.将下载的.woff文件在transfonter转为base64&#xff0c; 2.打开网站&#xff0c;导入文件&#xff0c;开启base64按钮&#xff0c;下载转换后的文件 3. 在下载解压后的文件夹中找到stylesheet.css&#xff0c;并复制其中的base64 4. 修改index.wxss文…

Jmeter +Maven+jenkins 接口性能全自动化测试

背景&#xff1a; 首先用jmeter录制或者书写性能测试的脚本&#xff0c;用maven添加相关依赖&#xff0c;把性能测试的代码提交到github&#xff0c;在jenkins配置git下载性能测试的代码&#xff0c;配置运行脚本和测试报告&#xff0c;配置运行失败自动发邮件通知&#xff0c…

高项V4.高级PM.项目集set+项目组合portfolio+组织级OPM+量化项目管理+实践模型

PMI &#xff0c; ITSS 、CMMI 和PRINCE2 等为各类信息系统项目管理提供了最佳实践&#xff0c;井提供了对组织的项目管理能力进行持续改进和评估的方法。 第一部分 项目集--《项目集管理标准>> (第4 版) ---实现项目11>2的更大效益 由项目管理协会(PMI) 出版的《…

快速制作美容行业预约小程序

随着科技的不断进步&#xff0c;移动互联网的快速发展&#xff0c;小程序成为了很多行业迅速发展的利器。对于美容行业来说&#xff0c;一款美容预约小程序不仅可以方便用户进行预约&#xff0c;还可以提升美容店铺的服务质量和管理效率。下面&#xff0c;我们来介绍一下如何快…

C高级第三讲

1、思维导图 2、输入一个文件名&#xff0c;判断是否为shell脚本文件&#xff0c;如果是脚本文件&#xff0c;判断是否有可执行权限&#xff0c;如果有可执行权限&#xff0c;运行文件&#xff0c;如果没有可执行权限&#xff0c;给文件添加可执行权限。 #!/bin/bash read -p …

vue 老项目 npm install 报错Python,c++等相关错误

​​​ 老项目npm install 下载依赖包报错 解决方法&#xff1a; //下载python 1、 npm install --global --production windows-build-tools//配置环境 &#xff1a; 也可暂时不用配置,能用就不用配置&#xff08;npm config set python "D:\Python27\python.exe&q…

康冠医疗2021笔试题

笔试时间:2020.09.24。 岗位:嵌入式软件工程师。 题型:13道题,40分钟。 6道填空,2道简答,5道编程,时间紧任务重。 1、填空 4、考察extern关键字。 6、const可以用来代替define ,define 只是简单的代替,但是const还会进行类型检查。 怎么避免头文件重复包含: #…

pandas read excel 更改string列为时间类型

设想我们有如下一个excel文件 我们都知道上面那个时间列其实是string类型&#xff0c;因此在用pandas做时间校验的时候会不通过&#xff0c;我们可以在read_excel的时候&#xff0c;指定这一列做转换 import pandas as pd from datetime import datetime, timedelta import n…

Mybatis 知识点

Mybatis 知识点 1.1 Mybatis 简介 1.1.1 什么是 Mybatis Mybatis 是一款优秀的持久层框架支持定制化 SQL、存储过程及高级映射Mybatis 几乎避免了所有的 JDBC 代码和手动设置参数以及获取结果集MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO…

flutter:占位视图(骨架屏、shimmer)

前言 有时候打开美团&#xff0c;在刚加载数据时会显示一个占位视图&#xff0c;如下&#xff1a; 那么这个是如何实现的呢&#xff1f;我们可以使用shimmer来开发该功能 实现 官方文档 https://pub-web.flutter-io.cn/packages/shimmer 安装 flutter pub add shimmer示例…

C语言----字节对齐

一&#xff1a;字节对齐的概念 针对字节对齐&#xff0c;百度百科的解释如下&#xff1a; 字节对齐是字节按照一定规则在空间上排列&#xff0c;字节(Byte)是计算机信息技术用于计量存储容量和传输容量的一种计量单位&#xff0c;一个字节等于8位二进制数&#xff0c;在UTF-8编…

[threejs]相机与坐标

搞清相机和坐标的关系在threejs初期很重要&#xff0c;否则有可能会出现写了代码&#xff0c;运行时一片漆黑的现象&#xff0c;这种情况就有可能是因为你相机没弄对。 先来看一下threejs中的坐标(世界坐标) 坐标轴好理解&#xff0c;大家只需要知道在three中不同颜色代表的轴…

mysql修改密码

文章目录 一、修改密码方式一&#xff1a;用SET PASSWORD命令方式二&#xff1a;用mysqladmin方式三&#xff1a;使用alter user语句 二、修改密码可能遇到的问题ERROR 1396 (HY000): Operation ALTER USERERROR 1064 (42000) 在mysql使用过程中&#xff0c;我们可能经常会对my…

pytorch的CrossEntropyLoss交叉熵损失函数默认reduction是平均值

pytorch中使用nn.CrossEntropyLoss()创建出来的交叉熵损失函数计算损失默认是求平均值的&#xff0c;即多个样本输入后获取的是一个均值标量&#xff0c;而不是样本大小的向量。 net nn.Linear(4, 2) loss nn.CrossEntropyLoss() X torch.rand(10, 4) y torch.ones(10, dt…

机器学习笔记之优化算法(六)线搜索方法(步长角度;非精确搜索;Glodstein Condition)

机器学习笔记之优化算法——线搜索方法[步长角度&#xff0c;非精确搜索&#xff0c;Glodstein Condition] 引言回顾&#xff1a; Armijo Condition \text{Armijo Condition} Armijo Condition关于 Armijo Condition \text{Armijo Condition} Armijo Condition的弊端 Glodstein…

海外版金融理财系统源码 国际投资理财系统源码 项目投资理财源码

海外版金融理财系统源码 国际投资理财系统源码 项目投资理财源码

WebRTC 之音视频同步

在网络视频会议中&#xff0c; 我们常会遇到音视频不同步的问题&#xff0c; 我们有一个专有名词 lip-sync 唇同步来描述这类问题&#xff0c;当我们看到人的嘴唇动作与听到的声音对不上的时候&#xff0c;不同步的问题就出现了 而在线会议中&#xff0c; 听见清晰的声音是优先…

【安装】阿里云轻量服务器安装Ubuntu图形化界面(端口号/灰屏问题)

阿里云官网链接 https://help.aliyun.com/zh/simple-application-server/use-cases/use-vnc-to-build-guis-on-ubuntu-18-04-and-20-04 网上搜了很多教程&#xff0c;但是我没在界面看到有vnc连接&#xff0c;后面才发现官网有教程。 其实官网很详细了&#xff0c;不过这里还是…

18、springboot默认的配置文件及导入额外配置文件

springboot默认的配置文件及导入额外配置文件 ★ Spring Boot默认加载的配置文件&#xff1a; (1) 类加载路径&#xff08;resources目录&#xff09;application.properties|yml &#xff08;相当于JAR包内&#xff09;optional: classpath:/ &#xff08;2&#xff09;类加…

钉钉对接打通金蝶云星空获取流程实例列表详情(宜搭)接口与其他应收单接口

钉钉对接打通金蝶云星空获取流程实例列表详情&#xff08;宜搭&#xff09;接口与其他应收单接口 对接系统钉钉 钉钉&#xff08;DingTalk&#xff09;是阿里巴巴集团专为中国企业打造的免费沟通和协同的多端平台&#xff0c;提供PC版&#xff0c;Web版和手机版&#xff0c;有考…