Java 重入锁 ReentrantLock 原理分析
1.简介
可重入锁ReentrantLock
自 JDK 1.5 被引入,功能上与synchronized
关键字类似。所谓的可重入是指,线程可对同一把锁进行重复加锁,而不会被阻塞住,这样可避免死锁的产生。ReentrantLock 的主要功能和 synchronized 关键字一致,均是用于多线程的同步。但除此之外,ReentrantLock 在功能上比 synchronized 更为丰富。比如 ReentrantLock 在加锁期间,可响应中断,可设置超时等。
ReentrantLock 是我们日常使用很频繁的一种锁,所以在使用之余,我们也应该去了解一下它的内部实现原理。ReentrantLock 内部是基于 AbstractQueuedSynchronizer(以下简称AQS
)实现的。所以要想理解 ReentrantLock,应先去 AQS 相关原理。我在之前的文章 AbstractQueuedSynchronizer 原理分析 - 独占/共享模式中,已经详细分析过 AQS 原理,有兴趣的朋友可以去看看。本文仅会在需要的时候对 AQS 相关原理进行简要说明,更详细的说明请参考我的其他文章。
2.原理
本章将会简单介绍重入锁 ReentrantLock 中的一些概念和相关原理,包括可重入、公平和非公平锁等原理。在介绍这些原理前,首先我会介绍 ReentrantLock 与 synchronized 关键字的相同和不同之处。在此之后才回去介绍重入、公平和非公平等原理。
2.1 与 synchronized 的异同
ReentrantLock 和 synchronized 都是用于线程的同步控制,但它们在功能上来说差别还是很大的。对比下来 ReentrantLock 功能明显要丰富的多。下面简单列举一下两者之间的差异,如下:
特性 | synchronized | ReentrantLock | 相同 |
---|---|---|---|
可重入 | 是 | 是 | ✅ |
响应中断 | 否 | 是 | ❌ |
超时等待 | 否 | 是 | ❌ |
公平锁 | 否 | 是 | ❌ |
非公平锁 | 是 | 是 | ✅ |
是否可尝试加锁 | 否 | 是 | ❌ |
是否是Java内置特性 | 是 | 否 | ❌ |
自动获取/释放锁 | 是 | 否 | ❌ |
对异常的处理 | 自动释放锁 | 需手动释放锁 | ❌ |
除此之外,ReentrantLock 提供了丰富的接口用于获取锁的状态,比如可以通过isLocked()
查询 ReentrantLock 对象是否处于锁定状态, 也可以通过getHoldCount()
获取 ReentrantLock 的加锁次数,也就是重入次数等。而 synchronized 仅支持通过Thread.holdsLock
查询当前线程是否持有锁。另外,synchronized 使用的是对象或类进行加锁,而 ReentrantLock 内部是通过 AQS 中的同步队列进行加锁,这一点和 synchronized 也是不一样的。
这里列举了不少两者的相同和不同之处,暂时这能想到这些。如果还有其他的区别,欢迎补充。
2.2 可重入
可重入这个概念并不难理解,本节通过一个例子简单说明一下。
现在有方法 m1 和 m2,两个方法均使用了同一把锁对方法进行同步控制,同时方法 m1 会调用 m2。线程 t 进入方法 m1 成功获得了锁,此时线程 t 要在没有释放锁的情况下,调用 m2 方法。由于 m1 和 m2 使用的是同一把可重入锁,所以线程 t 可以进入方法 m2,并再次获得锁,而不会被阻塞住。示例代码大致如下:
|
|
假如 lock 是不可重入锁,那么上面的示例代码必然会引起死锁情况的发生。这里请大家思考一个问题,ReentrantLock 的可重入特性是怎样实现的呢?简单说一下,ReentrantLock 内部是通过 AQS 实现同步控制的,AQS 有一个变量 state 用于记录同步状态。初始情况下,state = 0,表示 ReentrantLock 目前处于解锁状态。如果有线程调用 lock 方法进行加锁,state 就由0变为1,如果该线程再次调用 lock 方法加锁,就让其自增,即 state++。线程每调用一次 unlock 方法释放锁,会让 state–。通过查询 state 的数值,即可知道 ReentrantLock 被重入的次数了。这就是可重复特性的大致实现流程。
2.3 公平与非公平
公平与非公平指的是线程获取锁的方式。公平模式下,线程在同步队列中通过 FIFO 的方式获取锁,每个线程最终都能获取锁。在非公平模式下,线程会通过“插队”的方式去抢占锁,抢不到的则进入同步队列进行排队。默认情况下,ReentrantLock 使用的是非公平模式获取锁,而不是公平模式。不过我们也可通过 ReentrantLock 构造方法ReentrantLock(boolean fair)
调整加锁的模式。
既然既然有两种不同的加锁模式,那么他们有什么优缺点呢?答案如下:
公平模式下,可保证每个线程最终都能获得锁,但效率相对比较较低。非公平模式下,效率比较高,但可能会导致线程出现饥饿的情况。即一些线程迟迟得不到锁,每次即将到手的锁都有可能被其他线程抢了。这里再提个问题,为啥非公平模式抢了其他线程获取锁的机会,而整个程序的运行效率会更高呢?说实话,开始我也不明白。不过好在《Java并发编程实战》在第13.3节 公平性(p232)
说明了具体的原因,这里引用一下:
在激烈竞争的情况下,非公平锁的性能高于公平锁的性能的一个原因是:在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。假设线程 A 持有一个锁,并且线程 B 请求这个锁。由于这个线程已经被线程 A 持有,因此 B 将被挂起。当 A 释放锁时,B 将被唤醒,因此会再次尝试获取锁。与此同时,如果 C 也请求这个锁,那么 C 很有可能会在 B 被完全唤醒前获得、使用以及释放这个锁。这样的情况时一种“双赢”的局面:B 获得锁的时刻并没有推迟,C 更早的获得了锁,并且吞吐量也获得了提高。
上面的原因大家看懂了吗?下面配个图辅助说明一下:
如上图,线程 C 在线程 B 苏醒阶段内获取和使用锁,并在线程 B 获取锁前释放了锁,所以线程 B 可以顺利获得锁。线程 C 在抢占锁的情况下,仍未影响线程 B 获取锁,因此是个“双赢”的局面。
除了上面的原因外,《Java并发编程的艺术》在其5.3.2 公平与非公平锁的区别(p137)
分析了另一个可能的原因。即公平锁线程切换次数要比非公平锁线程切换次数多得多,因此效率上要低一些。更多的细节,可以参考作者的论述,这里不展开说明了。
本节最后说一下公平锁和非公平锁的使用场景。如果线程持锁时间短,则应使用非公平锁,可通过“插队”提升效率。如果线程持锁时间长,“插队”带来的效率提升可能会比较小,此时应使用公平锁。
3. 源码分析
3.1 代码结构
前面说到 ReentrantLock 是基于 AQS 实现的,AQS 很好的封装了同步队列的管理,线程的阻塞与唤醒等基础操作。基于 AQS 的同步组件,推荐的使用方式是通过内部非 public 静态类继承 AQS,并重写部分抽象方法。其代码结构大致如下:
上图中,Sync
是一个静态抽象类,继承了 AbstractQueuedSynchronizer。公平和非公平锁的实现类NonfairSync
和FairSync
则继承自 Sync 。至于 ReentrantLock 中的其他一些方法,主要逻辑基本上都在几个内部类中实现的。
3.2 获取锁
在分析 ReentrantLock 加锁的代码前,下来简单介绍一下 AQS 同步队列的一些知识。AQS 维护了一个基于双向链表的同步队列,线程在获取同步状态失败的情况下,都会被封装成节点,然后加入队列中。同步队列大致示意图如下:
在同步队列中,头结点是获取同步状态的节点。其他节点在尝试获取同步状态失败后,会被阻塞住,暂停运行。当头结点释放同步状态后,会唤醒其后继节点。后继节点会将自己设为头节点,并将原头节点从队列中移除。大致示意图如下:
介绍完 AQS 同步队列,以及节点线程获取同步状态的过程。下面来分析一下 ReentrantLock 中获取锁方法的源码,如下:
|
|
lock 方法的实现很简单,不过这里的 lock 方法只是一个壳子而已。由于获取锁的方式有公平和非公平之分,所以具体的实现是在NonfairSync
和FairSync
两个类中。那么我们继续往下分析一下这两个类的实现。
3.2.1 公平锁
公平锁对应的逻辑是 ReentrantLock 内部静态类 FairSync,我们沿着上面的 lock 方法往下分析,如下:
|
|
ReentrantLock 中获取锁的流程并不是很复杂,上面的代码执行流程如下:
- 调用 acquire 方法,将线程放入同步队列中进行等待
- 线程在同步队列中成功获取锁,则将自己设为持锁线程后返回
- 若同步状态不为0,且当前线程为持锁线程,则执行重入逻辑
3.2.2 非公平锁
分析完公平锁相关代码,下面再来看看非公平锁的源码分析,如下:
|
|
非公平锁的实现也不是很复杂,其加锁的步骤大致如下:
- 调用 compareAndSetState 方法抢占式加锁,加锁成功则将自己设为持锁线程,并返回
- 若加锁失败,则调用 acquire 方法,将线程置于同步队列尾部进行等待
- 线程在同步队列中成功获取锁,则将自己设为持锁线程后返回
- 若同步状态不为0,且当前线程为持锁线程,则执行重入逻辑
3.2.3 公平和非公平细节对比
如果大家之前阅读过公平锁和非公平锁的源码,会发现两者之间的差别不是很大。为了找出它们之间的差异,这里我将两者的对比代码放在一起,大家可以比较一下,如下:
从上面的源码对比图中,可以看出两种的差异并不大。那么现在请大家思考一个问题:在代码差异不大情况下,是什么差异导致了公平锁和非公平锁的产生呢?大家先思考一下,答案将会在下面展开说明。
在上面的源码对比图中,左边是非公平锁的实现,右边是公平锁的实现。从对比图中可看出,两者的 lock 方法有明显区别。非公平锁的 lock 方法会首先尝试去抢占设置同步状态,而不是直接调用 acquire 将线程放入同步队列中等待获取锁。除此之外,tryAcquire 方法实现上也有差异。由于非公平锁的 tryAcquire 逻辑主要封装在 Sync 中的 nonfairTryAcquire 方法里,所以我们直接对比这个方法即可。由上图可以看出,Sync 中的 nonfairTryAcquire 与公平锁中的 tryAcquire 实现上差异并不大,唯一的差异在第18行,这里我用一条红线标注了出来。公平锁的 tryAcquire 在第18行多出了一个条件,即!hasQueuedPredecessors()
。这个方法的目的是判断是否有其他线程比当前线程在同步队列中等待的时间更长。有的话,返回 true,否则返回 false。比如下图:
node1 对应的线程比 node2 对应的线程在队列中等待的时间更长,如果 node2 线程调用 hasQueuedPredecessors 方法,则会返回 true。如果 node1 调用此方法,则会返回 false。因为 node1 前面只有一个头结点,但头结点已经获取同步状态,不处于等待状态。所以在所有处于等待状态的节点中,没有节点比它等待的更长了。理解了 hasQueuedPredecessors 方法的用途后,那么现在请大家思考个问题,假如把条件去掉对公平锁会有什么影响呢?答案在 lock 所调用的 acquire 方法中,再来看一遍 acquire 方法源码:
|
|
acquire 方法先调用子类实现的 tryAcquire 方法,用于尝试获取同步状态,调用成功则直接返回。若调用失败,则应将线程插入到同步队列尾部,按照 FIFO 原则获取锁。如果我们把 tryAcquire 中的条件!hasQueuedPredecessors()
去掉,公平锁将不再那么“谦让”,它将会像非公平锁那样抢占获取锁,抢占失败才会入队。若如此,公平锁将不再公平。
3.3 释放锁
分析完了获取锁的相关逻辑,接下来再来分析一下释放锁的逻辑。与获取锁相比,释放锁的逻辑会简单一些,因为释放锁的过程没有公平和非公平之分。好了,下面开始分析 unlock 的逻辑:
|
|
重入锁的释放逻辑并不复杂,这里就不多说了。
4.总结
本文分析了可重入锁 ReentrantLock 公平与非公平获取锁以及释放锁原理,并与 synchronized 关键字进行了类比。总体来说,ReentrantLock 的原理在熟悉 AQS 原理的情况下,理解并不是很复杂。ReentrantLock 是大家经常使用的一个同步组件,还是很有必要去弄懂它的原理的。
好了,本文到这里就结束了。谢谢大家的阅读,再见。
参考
- 《Java并发编程实战》- Brian Goetz / Tim Peierls / Joshua Bloch / Joseph Bowbeer / David Holmes / Doug Lea
- 《Java并发编程的艺术》- 方腾飞 / 魏鹏 / 程晓明
- 本文链接: https://www.tianxiaobo.com/2018/05/07/Java-重入锁-ReentrantLock-原理分析/
from: http://www.tianxiaobo.com/2018/05/07/Java-%E9%87%8D%E5%85%A5%E9%94%81-ReentrantLock-%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90/
Java 重入锁 ReentrantLock 原理分析相关推荐
- java中的账户冻结原理_java可重入锁(ReentrantLock)的实现原理
前言 相信学过java的人都知道 synchronized 这个关键词,也知道它用于控制多线程对并发资源的安全访问,兴许,你还用过Lock相关的功能,但你可能从来没有想过java中的锁底层的机制是怎么 ...
- Java 可重入锁内存可见性分析
转载自 深度好文 | Java 可重入锁内存可见性分析 一个习以为常的细节 之前在做 ReentrantLock 相关的试验,试验本身很简单,和本文相关的简化版如下:(提示:以下代码均可左右滑动) p ...
- java多线程---重入锁ReentrantLock
1.定义 重入锁ReentrantLock,支持重入的锁,表示一个线程对资源的重复加锁. 2.底层实现 每个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获 ...
- 详述重入锁-ReentrantLock
什么是重入锁? 锁主要用来控制多线程访问的行为,对于同一个线程,如果连续两次对同一把锁进行lock,那么这个线程会被卡死在那里,这样的特性很不好,在实际的开发中,方法之间的调用方式错综复杂,如果不小心 ...
- 重入锁ReentrantLock详解
重入锁ReentrantLock,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁.除此之外,该锁的还支持获取锁时的公平和非公平性选择. 在AQS实现中,当一个线程调用Mute ...
- 并发编程-19AQS同步组件之重入锁ReentrantLock、 读写锁ReentrantReadWriteLock、Condition
文章目录 J.U.C脑图 ReentrantLock概述 ReentrantLock 常用方法 synchronized 和 ReentrantLock的比较 ReentrantLock示例 读写锁R ...
- 可重复锁ReentrantLock原理分析
可重入锁ReentrantLock实现层面依赖 一.CAS(compareAndSet) LockSupport 基本的方法 park park使得当前线程放弃cpu 进入等待(waiting)状态 ...
- JDK1.8源码分析:可重入锁ReentrantLock和Condition的实现原理
synchronized的用法和实现原理 synchronized实现线程同步的用法和实现原理 不足 synchronized在线程同步的使用方面,优点是使用简单,可以自动加锁和解锁,但是也存在一些不 ...
- Java多线程系列——深入重入锁ReentrantLock
简述 ReentrantLock 是一个可重入的互斥(/独占)锁,又称为"独占锁". ReentrantLock通过自定义队列同步器(AQS-AbstractQueuedSychr ...
最新文章
- IIS启动时出现0x8ffe2740的错误
- 使用JDBCTemplate实现与Spring结合,方法公用 ——接口(BaseDao)
- linux tomcat PermGen space
- java中类的方法分为 类方法_Java中类的方法成员分为()方法和实例方法。
- 数据结构——树状数组
- linux ssh反向代理
- Makefile 编写教程(由简至难)
- PS学习日记①初接触
- 《史蒂夫乔布斯传》部分读书摘录
- ME525+ 刷机工具及设置中心号码
- mkdir 创建目录命令
- VS2019 关闭安全检查
- httpwatch详解
- 掌握这几款高保真设计软件,百万年薪不再是梦想
- 【Typecho主题】情侣博客Brave主题源码
- 2022年嵌入式就业居然还挺香的,村口小花每次看见我都微笑
- Photoshop Elements 2023 for mac/win(PS简化版2023)​
- 二进制十进制十六进制转换_二进制数制到十进制数制的转换
- Linux运维部落 DNS服务,Linux运维部落
- LeetCode221210_135、剑指 Offer 58 - II. 左旋转字符串
热门文章
- Jackson学习二之集合类对象与JSON互相转化--转载
- spring security源码分析之一springSecurityFilterChain
- spring mvc DispatcherServlet详解之拾忆工具类utils
- 元宇宙iwemeta:元宇宙催生新的行业机会,看看你能抓住哪些机遇?
- 孙正义看未来30年:投资趋势,永远不会错
- 深入分析新加坡金管局区块链计划 Ubin
- 谈无人车安卓为时尚早,投自动驾驶有三个标准
- 基于deep learning的快速图像检索(Deep Learning of Binary Hash Codes for Fast Image Retrieval)
- Docker Review - dockerfile 实战_使用dockerfile制作tomcat镜像
- Dubbo和SpringCloud的区别