16.ReentrantLock全解读
大家好,我是王有志,欢迎和我聊技术,聊漂泊在外的生活。快来加入我们的Java提桶跑路群:共同富裕的Java人。
经历了AQS的前世和今生后,我们已经知道AQS是Java中提供同步状态原子管理,线程阻塞/唤醒,以及线程排队功能的同步器基础框架。那么我们今天就来学习通过AQS实现的ReentrantLock。按照惯例,先来看3道关于ReentrantLock的常见面试题:
什么是
ReentrantLock?ReentrantLock内部原理是怎样的?如何实现可重入性?ReentrantLock和synchronized有什么区别?该如何选择?
接下来,我会尽可能的通过剖析源码的方式为大家解答以上的题目。
ReentrantLock是什么?
ReentrantLock译为可重入锁,在《一文看懂并发编程中的锁》中我们解释过锁的可重入特性:同一线程可以多次加锁,即可以重复进入被锁定的逻辑中。
Doug Lea是这样描述ReentrantLock的:
A reentrant mutual exclusion {@link Lock} with the same basic behavior and semantics as the implicit monitor lock accessed using {@code synchronized} methods and statements, but with extended capabilities.
“A reentrant mutual exclusion Lock”说明ReentrantLock除了具有可重入的特性,还是一把互斥锁。接着看后面的内容,ReentrantLock与使用synchronized方法/语句有相同的基本行为和语义。最后的" but with extended capabilities"则表明了ReentrantLock具有更好的拓展能力。
那么可重入互斥锁就是ReentrantLock的全部吗?别急,我们接着往后看:
The constructor for this class accepts an optional fairness parameter. When set true, under contention, locks favor granting access to the longest-waiting thread. Otherwise this lock does not guarantee any particular access order.
ReentrantLock提供了公平/非公平两种模式,默认非公平模式,可以通过构造器参数指定公平模式。
好了,目前为止我们已经对ReentrantLock有了比较清晰的认知了,按照《一文看懂并发编程中的锁》中的分类,ReentrantLock本质是互斥锁,具有可重入特性,此外ReentrantLock还实现了公平和非公平两种模式。
ReentrantLock怎么用?
ReentrantLocak的使用非常简单:
ReentrantLock lock = new ReentrantLock();
lock.lock();
// 业务逻辑
lock.unlock();
通过无参构造器创建ReentrantLock对象后,调用lock和unlock进行加锁和解锁的操作。除了无参构造器外,ReentrantLock还提供了一个有参构造器:
// 无参构造器
public ReentrantLock() {
sync = new NonfairSync();
}
// 有参构造器
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
FairSync和NonfairSync是ReentrantLock的内部类,可以通过构造器来指定ReentrantLock的公平模式或非公平模式。具体实现我们先按下不表,先来看ReentrantLock中提供的其它方法。
加锁方法
除了常用的lock外,ReentrantLock还提供了3个加锁方法:
// 尝试获取锁
public boolean tryLock();
// 尝试获取锁,否则排队等候指定时间
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException;
// 尝试获取锁
public void lockInterruptibly() throws InterruptedException;
tryLock直接尝试获取锁,特点在于竞争失败时直接返回false,不会进入队列等待。重载方法tryLock(long timeout, TimeUnit unit)增加的在队列中的最大等待时间,如果锁竞争失败,会加入到等待队列中,再次尝试获取锁,直到超时或中断。
lockInterruptibly的特点是,调用thread.interrupt中断线程后抛出InterruptedException异常,结束竞争。虽然lock也允许中断线程,但它并不会抛出异常。
其他方法
除了常用的加锁方法外,ReentrantLock还提供了用于分析锁的方法:
| 方法声明 | 作用 |
|---|---|
public int getHoldCount() |
返回当前线程持有锁的次数,即当前线程重入锁的次数 |
public final int getQueueLength() |
返回等待获取锁的线程数量估算值 |
public final boolean hasQueuedThread(Thread thread) |
查询当前线程是否在等待获取锁 |
public final boolean hasQueuedThreads() |
是否有线程在等待获取锁 |
public final boolean isFair() |
是否为公平锁 |
public boolean isHeldByCurrentThread() |
当前线程是否持有锁 |
public boolean isLocked() |
锁是否被线程持有,即锁是否被使用 |
public Condition newCondition() |
创建条件对象 |
public int getWaitQueueLength(Condition condition) |
等待在该条件上的线程数量 |
public boolean hasWaiters |
是否有线程在等待在该条件上 |
ReentrantLock源码分析
接下来,我们通过源码来分析ReentrantLock的公平/非公平模式,以及重入性的实现原理,并对比不同的加锁方法的实现差异。
ReentrantLock的结构
我们先来来了解下ReentrantLock的结构:
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
// 同步器
abstract static class Sync extends AbstractQueuedSynchronizer {}
// 非公平模式同步器
static final class NonfairSync extends Sync {}
// 公平模式同步器
static final class FairSync extends Sync {}
}
ReentrantLock仅仅实现了Lock接口,并没有直接继承AbstractQueuedSynchronizer,其内部类Sync继承AbstractQueuedSynchronizer,并提供了FairSync和NonfairSync两种实现,分别是公平锁和非公平锁。
公平/非公平模式
我们已经知道,可以指定不同的参数来创建公平/非公平模式的ReentrantLock,反应到源码中是使用不同的Sync的实现类:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
并且在加锁/解锁操作中,均由Sync的实现类完成,ReentrantLock只是对Lock接口的实现:
public class ReentrantLock implements Lock, java.io.Serializable {
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
}
先来回想下《AQS的今生,构建出JUC的基础》中的acquire方法:
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
}
AQS自身仅实现了将线程添加到等待队列中的acquireQueued方法,而预留了获取锁的tryAcquire方法。
那么我们不难想到,ReentrantLock的作用机制:继承自AQS的Sync,实现了tryAcquire方法来获取锁,并借助AQS的acquireQueued实现排队的功能,而ReentrantLock的公平与否,与tryAcquire的实现方式是息息相关的。
公平锁FairSync
FairSync非常简单,仅做了tryAcquire方法的实现:
static final class FairSync extends Sync {
@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 获取同步状态,AQS实现
int c = getState();
// 判断同步状态
// c == 0时,表示没有线程持有锁
// c != 0时,表示有线程持有锁
if (c == 0) {
// hasQueuedPredecessors判断是否有已经在等待锁的线程
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
// 线程重入,同步状态+1
int nextc = c + acquires;
if (nextc < 0) {
throw new Error("Maximum lock count exceeded");
}
// 更新同步状态
setState(nextc);
return true;
}
return false;
}
}
当c == 0时,锁未被任何线程持有,通过hasQueuedPredecessors判断是否已经有等待锁的线程,如果没有正在等待的线程,则通过compareAndSetState(0, acquires)尝试替换同步状态来获取锁;当c != 0时,锁已经被线程持有,通过current == getExclusiveOwnerThread判断是否为当前线程持有,如果是则认为是重入,执行int nextc = c + acquires,更新同步状态setState(nextc),并返回成功。
FairSync的公平性体现在获取锁前先执行hasQueuedPredecessors,确认是否已经有线程在等待锁,如果有则tryAcquire执行失败,默默的执行AQS的acquireQueued加入等待队列中即可。
非公平锁NonfairSync
NonfairSync也只是做了tryAcquire的实现,而且还只是掉用了父类的nonfairTryAcquire方法:
static final class NonfairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
abstract static class Sync extends AbstractQueuedSynchronizer {
@ReservedStackAccess
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) {
throw new Error("Maximum lock count exceeded");
}
setState(nextc);
return true;
}
return false;
}
}
nonfairTryAcquire与FairSync#tryAcquire简直是一模一样,忽略方法声明,唯一的差别就在于,当c == 0时,nonfairTryAcquire并不会调用hasQueuedPredecessors确认是否有线程正在等待获取锁,而是直接通过compareAndSetState(0, acquires)尝试替换同步状态来获取锁。
NonfairSync的不公平体现在获取锁前不会不会确认是否有线程正在等待锁,而是直接获取锁,如果获取失败,依旧会执行AQS的acquireQueued加入等待队列。
可重入性的实现
《AQS的今生,构建出JUC的基础》中,提到过ReentrantLock的重入性依赖于同步状态state作为计数器的特性实现,在公平锁FairSync和非公平锁NonfairSync的实现中我们也看到,线程重入时会执行同步状态+1的操作:
int nextc = c + acquires;
setState(nextc);
既然lock操作中有同步状态+1的操作,那么unlock操作中就一定有同步状态-1的操作:
public class ReentrantLock implements Lock, java.io.Serializable {
public void unlock() {
sync.release(1);
}
abstract static class Sync extends AbstractQueuedSynchronizer {
@ReservedStackAccess
protected final boolean tryRelease(int releases) {
// 线程退出,同步状态-1
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread()) {
throw new IllegalMonitorStateException();
}
boolean free = false;
if (c == 0) {
// 同步状态为0,锁未被持有,释放独占锁
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
}
}
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0){
unparkSuccessor(h);
}
return true;
}
return false;
}
}
tryRelease的实现并不复杂,同步状态-1后,如果同步状态为0,表示锁未被持有,修改锁的独占线程,然后更新同步状态。
我们再来看ReentrantLock的可重入性的实现,是不是非常简单了?判断是否是线程重入依赖的是getExclusiveOwnerThread方法,获取当前独占锁的线程,记录重入次数依赖的是同步状态作为计数器的特性。
现在能够理解为什么ReentrantLock中lock要与unlock操作成对出现了吧?最后,提个小问题,为什么lock和unlock操作中,只有当c == 0时的lock操作需要使用CAS?
加锁方法的差异
我们前面已经了解过ReentrantLock提供的4个加锁方法了,分别是:
public void lock(),最常用的加锁方法,允许中断,但不会抛出异常,加锁失败进入等待队列;public void lockInterruptibly(),允许中断,抛出InterruptedException异常,加锁失败进入队列直到被唤醒或者被中断;public boolean tryLock(),尝试直接加锁,加锁失败不会进入队列,而是直接返回false;public boolean tryLock(long timeout, TimeUnit unit),尝试直接加锁,中断时抛出InterruptedException异常,加锁失败进入队列,直到指定时间内加锁成功,或者超时。
lock与lockInterruptibly
lock方法的调用:
public class ReentrantLock implements Lock, java.io.Serializable {
public void lock() {
sync.acquire(1);
}
}
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
}
lockInterruptibly方法的调用:
public class ReentrantLock implements Lock, java.io.Serializable {
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
}
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
public final void acquireInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted()) {
throw new InterruptedException();
}
if (!tryAcquire(arg)) {
doAcquireInterruptibly(arg);
}
}
}
可以看到,差异主要体现在acquireQueued和doAcquireInterruptibly的实现上:
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
return interrupted;
}
// 当parkAndCheckInterrupt为true时,修改interrupted标记为中断
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
if (interrupted)
selfInterrupt();
throw t;
}
}
private void doAcquireInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
return;
}
// 当parkAndCheckInterrupt为true时,抛出异常
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
从源码上来看,差异体现在对parkAndCheckInterrupt结果的处理方式上,acquireQueued只标记中断状态,而doAcquireInterruptibly直接抛出异常。
tryLock与它的重载方法
public boolean tryLock()的实现非常简单:
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
直接调用Sync#nonfairTryAcquire,在前面非公平锁的内容中我们已经知道nonfairTryAcquire只是进行了一次非公平的加锁尝试,如果没有调用AQS的acquireQueued不会加入到等待队列中。
tryLock的重载方法也并不复杂,按照之前的习惯,应该是有着特殊的acquireQueued实现:
public class ReentrantLock implements Lock, java.io.Serializable {
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
}
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
if (Thread.interrupted()) {
throw new InterruptedException();
}
return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);
}
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
// 计算超时时间
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
return true;
}
// 判断超时时间
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L) {
cancelAcquire(node);
return false;
}
// 调用LockSupport.parkNanos暂停指定时间
if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD)
LockSupport.parkNanos(this, nanosTimeout);
// 线程中断抛出异常
if (Thread.interrupted())
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
}
public boolean tryLock(long timeout, TimeUnit unit)的特性依赖于LockSupport.parkNanos暂停线程指定时间的能力。另外,我们可以注意到在判断是否需要park时,对nanosTimeout与SPIN_FOR_TIMEOUT_THRESHOLD的判断:
当
nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD时,认为一次park和upark对性能的影响小于自旋nanosTimeout纳秒;当
nanosTimeout < SPIN_FOR_TIMEOUT_THRESHOLD时,认为一次park和upark对性能的影响大于自旋nanosTimeout纳秒。
到这里我们就把4个加锁方法的差异讲完了,大体逻辑是相似的(如,唤醒头节点),只是为了实现某些特性添加了一些细节,大家可以认真阅读源码,很容易就能看出差异。
结语
关于ReentrantLock的内容到这里就结束了,因为已经把AQS的部分单独拆了出来,所以今天并没有太复杂的内容。大家的重点可以放在ReentrantLock是如何借助AQS实现公平/非公平模式,以及可重入的特性上,诸如getHoldCount,isFair这类方法,相信大家已经能够想象到是如何实现的了,可以结合源码验证自己的想法。
最后,希望今天的内容能够帮助你更清晰的理解ReentrantLock,如果文章中出现错误,也希望大家不吝赐教。
好了,今天就到这里了,Bye~~
16.ReentrantLock全解读的更多相关文章
- ToF技术全解读
文章目录 ToF技术全解读 1. 什么是ToF 2. ToF的原理 3. ToF 优缺点 ToF技术全解读 1. 什么是ToF ToF: Time of flight. 飞行时间.当然这只是一种翻译的 ...
- 移动GPU全解读(二)
[编者按]:本文作者为爱搞机特约作者.技术达人"炮神"@ioncannon. 在上一篇移动GPU解读中,对移动GPU的架构.相关參数进行了介绍,本部分介绍的则是移动GPU的Shad ...
- hive 压缩全解读(hive表存储格式以及外部表直接加载压缩格式数据);HADOOP存储数据压缩方案对比(LZO,gz,ORC)
数据做压缩和解压缩会增加CPU的开销,但可以最大程度的减少文件所需的磁盘空间和网络I/O的开销,所以最好对那些I/O密集型的作业使用数据压缩,cpu密集型,使用压缩反而会降低性能. 而hive中间结果 ...
- WannaCry勒索病毒全解读,权威修复指南大集合
多地的出入境.派出所等公安网络疑似遭遇了勒索蠕虫病毒袭击,已暂时停办出入境业务:加油站突然断网,不能支持支付宝.微信.银联卡等联网支付:大批高校师生电脑中的文件被蠕虫病毒加密,需要支付相应的赎金方可解 ...
- Java NIO 粘包 拆包 (实战) - 史上最全解读
疯狂创客圈 Java 聊天程序[ 亿级流量]实战系列之13 [博客园 总入口 ] 本文的源码工程:Netty 粘包/半包原理与拆包实战 源码 本实例是<Netty 粘包/半包原理与拆包实战> ...
- Spring Boot 最流行的 16 条实践解读,你值得收藏!
Spring Boot是最流行的用于开发微服务的Java框架.在本文中,我将与你分享自2016年以来我在专业开发中使用Spring Boot所采用的最佳实践.这些内容是基于我的个人经验和一些熟知的Sp ...
- Spring Boot 最流行的 16 条实践解读!【华为云技术分享】
置顶:华为云618大促火热进行中,全场1折起,免费抽主机,消费满额送P30 Pro,点此抢购. Spring Boot是最流行的用于开发微服务的Java框架.在本文中,将与大家分享自2016年以来笔者 ...
- Spring Boot 最流行的 16 条实践解读!
Spring Boot是最流行的用于开发微服务的Java框架.在本文中,我将与你分享自2016年以来我在专业开发中使用Spring Boot所采用的最佳实践.这些内容是基于我的个人经验和一些熟知的Sp ...
- Netty 粘包 拆包 | 史上最全解读
Netty 粘包/半包原理与拆包实战(史上最全) 疯狂创客圈 Java 聊天程序[ 亿级流量]实战系列之13 [博客园 总入口 ] 本文的源码工程:Netty 粘包/半包原理与拆包实战 源码 本实例是 ...
- 4K超清,2500万人在线,猫晚直播技术全解读
摘要: 作为双11的必备节目,今年的猫晚通过优酷.浙江卫视.东方卫视进行了全程网络直播和电视直播,吸引了超过全球超过2.4亿人收看.猫晚期间,优酷基于阿里云最新的广播级高可靠直播方案,为近2500万的 ...
随机推荐
- 01 docker容器技术基础入门
本章内容: 1.container是什么? 2.LXC技术介绍 3.namespaces-名称空间,实现资源隔离 4.容器的资源分配--Cgroup,实现资源分配 5.LXC与dockers ---- ...
- 关于uni-app开发的微信小程序顶部导航条机型适配
背景: 小程序顶部导航栏那里的样式和功能都是小程序自带的,当我们在pages.json里的pages里新加一条页面配置时,会自动生成一个带顶部导航栏的空白页面,当然也可以再配置里"navig ...
- Mac 环境下 编译 spring 源码
环境:macos idea jdk1.8 首先,在 spring.io的 git 地址 上下载下来源码后,执行里面的 gradlew命令,一般只要网络没有问题,都是可以成功的 然后,看显示的 Welc ...
- k8s探针
探针是由kubelet对容器执行的定期诊断.要执行诊断,kubelet调用由容器实现的Handler.有三类处理程序: ExecAction:在容器内执行指定命令.如果命令退出时返回码为0认为诊断成功 ...
- CSS clip-path 属性
属性定义及使用说明 clip-path 属性使用裁剪方式创建元素的可显示区域.区域内的部分显示,区域外的隐藏.可以指定一些特定形状. 注意: clip-path 属性将替换已弃用的 clip 属性. ...
- 使用VSCode调试C#时,Console.ReadLine()弹出命令框调试
原文链接:https://blog.csdn.net/qq_29503199/article/details/88351498 要在调试时读取输入,可以在 launch.json 中使用配置中的 ...
- 痞子衡嵌入式:RISC-V指令集架构MCU开发那些事 - 索引
大家好,我是痞子衡,是正经搞技术的痞子.本系列痞子衡给大家介绍的是RISC-V指令集架构微控制器相关知识. RISC-V指令集最早要追溯到2010年,是加州大学伯克利分校的一个研究团队的项目,目标是设 ...
- 【QCustomPlot】简介
说明 使用 QCustomPlot 绘图库辅助开发时整理的学习笔记. 目录 说明 1. 库简介 2. 库的官网链接 3. 库的帮助文档 4. 库的下载地址 5. 库的版本号说明 6. 库的 Git 地 ...
- Linux & 标准C语言学习 <DAY5>
一.if分支语句 if(表达式) //单分支语句 { //表达式的值为真,则执行此处代码 } if(表达式) //双分支语句 { ...
- 剑指 offer 第 4 天
第 4 天 查找算法(简单) 剑指 Offer 03. 数组中重复的数字 找出数组中重复的数字. 在一个长度为 n 的数组 nums 里的所有数字都在 0-n-1 的范围内.数组中某些数字是重复的,但 ...