大家好,我是王有志,欢迎和我聊技术,聊漂泊在外的生活。快来加入我们的Java提桶跑路群:共同富裕的Java人

经历了AQS的前世今生后,我们已经知道AQS是Java中提供同步状态原子管理,线程阻塞/唤醒,以及线程排队功能的同步器基础框架。那么我们今天就来学习通过AQS实现的ReentrantLock。按照惯例,先来看3道关于ReentrantLock的常见面试题:

  • 什么是ReentrantLock?

  • ReentrantLock内部原理是怎样的?如何实现可重入性?

  • ReentrantLocksynchronized有什么区别?该如何选择?

接下来,我会尽可能的通过剖析源码的方式为大家解答以上的题目。

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对象后,调用lockunlock进行加锁和解锁的操作。除了无参构造器外,ReentrantLock还提供了一个有参构造器:

// 无参构造器
public ReentrantLock() {
sync = new NonfairSync();
} // 有参构造器
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

FairSyncNonfairSyncReentrantLock的内部类,可以通过构造器来指定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,并提供了FairSyncNonfairSync两种实现,分别是公平锁和非公平锁。

公平/非公平模式

我们已经知道,可以指定不同的参数来创建公平/非公平模式的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;
}
}

nonfairTryAcquireFairSync#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方法,获取当前独占锁的线程,记录重入次数依赖的是同步状态作为计数器的特性。

现在能够理解为什么ReentrantLocklock要与unlock操作成对出现了吧?最后,提个小问题,为什么lockunlock操作中,只有当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);
}
}
}

可以看到,差异主要体现在acquireQueueddoAcquireInterruptibly的实现上:

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时,对nanosTimeoutSPIN_FOR_TIMEOUT_THRESHOLD的判断:

  • nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD时,认为一次park和upark对性能的影响小于自旋nanosTimeout纳秒;

  • nanosTimeout < SPIN_FOR_TIMEOUT_THRESHOLD时,认为一次park和upark对性能的影响大于自旋nanosTimeout纳秒。

到这里我们就把4个加锁方法的差异讲完了,大体逻辑是相似的(如,唤醒头节点),只是为了实现某些特性添加了一些细节,大家可以认真阅读源码,很容易就能看出差异。

结语

关于ReentrantLock的内容到这里就结束了,因为已经把AQS的部分单独拆了出来,所以今天并没有太复杂的内容。大家的重点可以放在ReentrantLock是如何借助AQS实现公平/非公平模式,以及可重入的特性上,诸如getHoldCountisFair这类方法,相信大家已经能够想象到是如何实现的了,可以结合源码验证自己的想法。

最后,希望今天的内容能够帮助你更清晰的理解ReentrantLock,如果文章中出现错误,也希望大家不吝赐教。


好了,今天就到这里了,Bye~~

16.ReentrantLock全解读的更多相关文章

  1. ToF技术全解读

    文章目录 ToF技术全解读 1. 什么是ToF 2. ToF的原理 3. ToF 优缺点 ToF技术全解读 1. 什么是ToF ToF: Time of flight. 飞行时间.当然这只是一种翻译的 ...

  2. 移动GPU全解读(二)

    [编者按]:本文作者为爱搞机特约作者.技术达人"炮神"@ioncannon. 在上一篇移动GPU解读中,对移动GPU的架构.相关參数进行了介绍,本部分介绍的则是移动GPU的Shad ...

  3. hive 压缩全解读(hive表存储格式以及外部表直接加载压缩格式数据);HADOOP存储数据压缩方案对比(LZO,gz,ORC)

    数据做压缩和解压缩会增加CPU的开销,但可以最大程度的减少文件所需的磁盘空间和网络I/O的开销,所以最好对那些I/O密集型的作业使用数据压缩,cpu密集型,使用压缩反而会降低性能. 而hive中间结果 ...

  4. WannaCry勒索病毒全解读,权威修复指南大集合

    多地的出入境.派出所等公安网络疑似遭遇了勒索蠕虫病毒袭击,已暂时停办出入境业务:加油站突然断网,不能支持支付宝.微信.银联卡等联网支付:大批高校师生电脑中的文件被蠕虫病毒加密,需要支付相应的赎金方可解 ...

  5. Java NIO 粘包 拆包 (实战) - 史上最全解读

    疯狂创客圈 Java 聊天程序[ 亿级流量]实战系列之13 [博客园 总入口 ] 本文的源码工程:Netty 粘包/半包原理与拆包实战 源码 本实例是<Netty 粘包/半包原理与拆包实战> ...

  6. Spring Boot 最流行的 16 条实践解读,你值得收藏!

    Spring Boot是最流行的用于开发微服务的Java框架.在本文中,我将与你分享自2016年以来我在专业开发中使用Spring Boot所采用的最佳实践.这些内容是基于我的个人经验和一些熟知的Sp ...

  7. Spring Boot 最流行的 16 条实践解读!【华为云技术分享】

    置顶:华为云618大促火热进行中,全场1折起,免费抽主机,消费满额送P30 Pro,点此抢购. Spring Boot是最流行的用于开发微服务的Java框架.在本文中,将与大家分享自2016年以来笔者 ...

  8. Spring Boot 最流行的 16 条实践解读!

    Spring Boot是最流行的用于开发微服务的Java框架.在本文中,我将与你分享自2016年以来我在专业开发中使用Spring Boot所采用的最佳实践.这些内容是基于我的个人经验和一些熟知的Sp ...

  9. Netty 粘包 拆包 | 史上最全解读

    Netty 粘包/半包原理与拆包实战(史上最全) 疯狂创客圈 Java 聊天程序[ 亿级流量]实战系列之13 [博客园 总入口 ] 本文的源码工程:Netty 粘包/半包原理与拆包实战 源码 本实例是 ...

  10. 4K超清,2500万人在线,猫晚直播技术全解读

    摘要: 作为双11的必备节目,今年的猫晚通过优酷.浙江卫视.东方卫视进行了全程网络直播和电视直播,吸引了超过全球超过2.4亿人收看.猫晚期间,优酷基于阿里云最新的广播级高可靠直播方案,为近2500万的 ...

随机推荐

  1. 01 docker容器技术基础入门

    本章内容: 1.container是什么? 2.LXC技术介绍 3.namespaces-名称空间,实现资源隔离 4.容器的资源分配--Cgroup,实现资源分配 5.LXC与dockers ---- ...

  2. 关于uni-app开发的微信小程序顶部导航条机型适配

    背景: 小程序顶部导航栏那里的样式和功能都是小程序自带的,当我们在pages.json里的pages里新加一条页面配置时,会自动生成一个带顶部导航栏的空白页面,当然也可以再配置里"navig ...

  3. Mac 环境下 编译 spring 源码

    环境:macos idea jdk1.8 首先,在 spring.io的 git 地址 上下载下来源码后,执行里面的 gradlew命令,一般只要网络没有问题,都是可以成功的 然后,看显示的 Welc ...

  4. k8s探针

    探针是由kubelet对容器执行的定期诊断.要执行诊断,kubelet调用由容器实现的Handler.有三类处理程序: ExecAction:在容器内执行指定命令.如果命令退出时返回码为0认为诊断成功 ...

  5. CSS clip-path 属性

    属性定义及使用说明 clip-path 属性使用裁剪方式创建元素的可显示区域.区域内的部分显示,区域外的隐藏.可以指定一些特定形状. 注意: clip-path 属性将替换已弃用的 clip 属性. ...

  6. 使用VSCode调试C#时,Console.ReadLine()弹出命令框调试

    原文链接:https://blog.csdn.net/qq_29503199/article/details/88351498   要在调试时读取输入,可以在 launch.json 中使用配置中的 ...

  7. 痞子衡嵌入式:RISC-V指令集架构MCU开发那些事 - 索引

    大家好,我是痞子衡,是正经搞技术的痞子.本系列痞子衡给大家介绍的是RISC-V指令集架构微控制器相关知识. RISC-V指令集最早要追溯到2010年,是加州大学伯克利分校的一个研究团队的项目,目标是设 ...

  8. 【QCustomPlot】简介

    说明 使用 QCustomPlot 绘图库辅助开发时整理的学习笔记. 目录 说明 1. 库简介 2. 库的官网链接 3. 库的帮助文档 4. 库的下载地址 5. 库的版本号说明 6. 库的 Git 地 ...

  9. Linux & 标准C语言学习 <DAY5>

    一.if分支语句     if(表达式)  //单分支语句     {           //表达式的值为真,则执行此处代码     }     if(表达式)  //双分支语句     {     ...

  10. 剑指 offer 第 4 天

    第 4 天 查找算法(简单) 剑指 Offer 03. 数组中重复的数字 找出数组中重复的数字. 在一个长度为 n 的数组 nums 里的所有数字都在 0-n-1 的范围内.数组中某些数字是重复的,但 ...