前言

在阅读完 JUC 包下的 AQS 源码之后,其中有很多疑问,最大的疑问就是 state 究竟是什么含义?并且 AQS 主要定义了队列的出入,但是获取资源、释放资源都是交给子类实现的,那子类是怎么实现的呢?下面开始了解 ReentrantLock。

公众号:『 刘志航 』,记录工作学习中的技术、开发及源码笔记;时不时分享一些生活中的见闻感悟。欢迎大佬来指导!

介绍

一个可重入的互斥锁与隐式监视器锁synchronized具有相同的基本行为和语义,但功能更强大。

具有以下特征:

  1. 互斥性:同时只有一个线程可以获取到该锁,此时其他线程请求获取锁,会被阻塞,然后被放到该锁内部维护的一个 AQS 阻塞队列中。
  2. 可重入性:维护 state 变量,初始为 0,当一个线程获取到锁时,state 使用 cas 更新为 1,本线程再次申请获取锁,会对 state 进行 CAS 递增,重复获取次数即 state,最多为 2147483647 。试图超出此限制会从锁定方法抛出 Error。
  3. 公平/非公平性:在初始化时,可以通过构造器传参,指定是否为公平锁,还是非公平锁。当设置为 true 时,为公平锁,线程争用锁时,会倾向于等待时间最长的线程。

基本使用

class X {
private final ReentrantLock lock = new ReentrantLock();
// ... public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}

问题疑问?

首先在阅读本文时,对 AQS 有了一定的了解,如果不了解的话,可以看一下之前的文章。图文讲解 AQS

  1. 在 AQS 中介绍 state 时,说 state 含义由子类进行定义,那在 ReentrantLock 中 state 代表什么?
  2. ReentrantLock 和 AQS 有什么关系?
  3. 线程是如何获取到锁的?
  4. 锁的可重入性是如何实现的?
  5. 当前线程获取锁失败,被阻塞的后续操作是什么?
  6. 公平锁和非公平锁是如何体现的?
  7. 锁是如何释放的?

将通过源码及画图的方式,围绕上面几个问题,展开阅读和分析。

源码分析

基本结构

基本结构如图所示,ReentrantLock 类实现了接口 Lock,在接口 Lock 中定义了使用锁时的方法,方法及含义如下:

public interface Lock {

    // 获取锁,如果没有获取到,会阻塞。
void lock(); // 获取锁,如果没有获取到,会阻塞。响应中断。
void lockInterruptibly() throws InterruptedException; // 尝试获取锁,如果获取到,返回 true,没有获取到 返回 false
boolean tryLock(); // 尝试获取锁,没有有获取到,会等待指定时间,响应中断。
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 释放锁
void unlock();
}

而 ReentrantLock 也只是实现了 Lock 接口,并实现了这些方法,那 ReentrantLock 和 AQS 到底有什么关系呢?这就需要看内部具体如何实现的了。

通过上面类图可以看出,在 ReentrantLock 中含有两个内部类,分别是 NonfairSync FairSync 而它俩又实现了 抽象类 Sync,抽象类 Sync 继承了 AbstractQueuedSynchronizer 即 AQS。具体代码如下:

public class ReentrantLock implements Lock, java.io.Serializable {

    private final Sync sync;

    // 锁的同步控制基础类。 子类具体到公平和非公平的版本。 使用AQS状态来表示持有该锁的数量。
abstract static class Sync extends AbstractQueuedSynchronizer {
// 省略 ...
} static final class NonfairSync extends Sync {
// 非公平锁逻辑 省略 ...
} static final class FairSync extends Sync {
// 公平锁逻辑 省略 ...
}
// 默认非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
// 根据传参指定公平锁还是非公平锁,true 公平锁,false 非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
}

通过上面代码可以看出:

  1. 锁的基本控制是由 NonfairSync 和 FairSync 进行控制的,而它俩的父类 Sync 继承了 AQS (AbstractQueuedSynchronizer),这也就是说明 ReentrantLock 的实现和 AQS 是有关的。
  2. NonfairSync 代表非公平锁实现逻辑,FairSync 代表公平锁实现逻辑。
  3. 构造器传参可以看出,初始化时,默认为 NonfairSync 非公平锁。也可以指定声明为公平锁或非公平锁,传参 true 为 公平锁,false 为非公平锁。

具体 ReentrantLock 和 AQS 的关系是怎样的,就需要通过加锁的过程来分析了。

lock

如图所示,默认声明非公平锁,lock 方法内部调用 sync.lock(); 此时应该是使用的非公平锁内部的 lock 加锁操作。

final void lock() {
// 通过 CAS 设置 state 值 0 -> 1
if (compareAndSetState(0, 1))
// 设置成功当前线程获取到了锁
setExclusiveOwnerThread(Thread.currentThread());
else
// 设置失败,则调用 AQS 的方法,尝试获取锁。
acquire(1);
}
  1. 首先会 使用 CAS 更新 state 的值, 此时就会发现, state 在这里代表的锁的状态。 0 未加锁,1 加锁。
  2. 设置失败,会调用 AQS 的 acquire(1); 方法。

再看下 AQS 的 acquire 代码:

public final void acquire(int arg) {
// tryAcquire 尝试获取 state,获取失败则会加入到队列
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

在之前分析 AQS 源码时,已经介绍 tryAcquire 是尝试获取 state 的值,AQS 中并不提供可用的方法,此处是由子类实现的。所以这块代码还是在 NonfairSync 类中自己实现的业务逻辑。

static final class NonfairSync extends Sync {
// NonfairSync 实现
protected final boolean tryAcquire(int acquires) {
// 调用父类的方法
return nonfairTryAcquire(acquires);
}
}
abstract static class Sync extends AbstractQueuedSynchronizer {
// NonfairSync 的父类 Sync 中有实现
// state 传参是 1
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取 state
int c = getState();
// 如果 c 是 0
if (c == 0) {
// 使用 cas 更新为 1
if (compareAndSetState(0, acquires)) {
// 设置持有线程为当前
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
// 如果是当前线程持有
// 对 state 进行累加
int nextc = c + acquires;
// 不允许超过 int 的最大值 2147483647 + 1 = -2147483648
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 设置 state 的值
setState(nextc);
return true;
}
return false;
}
}
  1. 当前线程加锁,直接使用 CAS 方式对 state 从 0 更新为 1,更新成功,则获得锁,更新失败,则获取失败。
  2. 更新失败后会调用 AQS 的 acquire(1); 方法, 此处传参为 1。
  3. tryAcquire 再次尝试获取锁。
    1. state 是 0,尝试获取。获取成功返回 true;
    2. state 不是 0,判断是否为当前线程持有,是当前线程持有则对 state 进行累加。
  4. tryAcquire 获取锁失败,则走 AQS 的 acquireQueued 逻辑,创建节点,并加入到等待队列中。

流程画图如下:

  • 初始为单个线程

  • 此时其他线程来请求获取锁

  • 加锁流程图

公平锁是如何体现的

static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L; final void lock() {
acquire(1);
} protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 判断有无节点排队
if (!hasQueuedPredecessors() &&
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;
}
}

拉出来代码比较一下:

可以看出在公平锁(FairSync)中多了一个判断条件

!hasQueuedPredecessors()

hasQueuedPredecessors 方法在 AQS 中,如果有当前线程前面的线程排队返回true,如果当前线程是在队列的头部或队列为空,返回false。

代码如下:

public final boolean hasQueuedPredecessors() {

    Node t = tail;
Node h = head;
Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}

如果当前加锁时已经有节点在排队,那就去节点尾部排队,否则才会去抢占锁。

到这里基本上已经知道公平锁和非公平锁的区别了:

非公平锁:不管有没有节点在排队,都会试图去获取锁,如果获取失败,进入 acquire 方法,还是会试图获取一次,之后才会进入队列中。

公平锁:已经有节点在排队,那就自己去节点后面排队。

tryLock


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

直接调用的 Sync 中的 nonfairTryAcquire, 尝试获取锁,获取失败,就返回 false,获取到锁或者是当前线程持有锁则对 state 累加后都返回 true。

unlock

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

发现 unlock 直接调用的 AQS 的 release 方法,进行释放资源。

public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

这块在 AQS 中有介绍,也说明 tryRelease 由子类进行实现,现在在 ReentrantLock 重点关注 tryRelease 的实现。

// 释放资源,传入值为 1
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
  1. 获取当前的 state 进行 -1 操作;
  2. 判断了下当前线程是否为持有线程;
  3. 如果释放完之后 state 为 0 ,则设置持有线程为 null;
  4. 更新并返回 state 的值。

总结

通过上面的源码及画图,基本上对开始的问题已经有了答案:

Q:在 AQS 中介绍 state 时,说 state 含义由子类进行定义,那在 ReentrantLock 中 state 代表什么?

A:在 ReentrantLock 中 state 代表加锁状态,0 没有线程获得锁,大于等于 1 已经有线程获得锁,大于 1 说明该获得锁的线程多次重入。

Q:ReentrantLock 和 AQS 有什么关系?

A:ReentrantLock 内部基于 AQS 实现,无论是锁状态,还是进入等待队列,锁释放等都是基于 AQS 实现。ReentrantLock 的公平锁和非公平锁都是 NonfairSync、FairSync 来实现的,而他们的父类 Sync 继承了 AQS。

Q:线程是如何获取到锁的?

A:线程通过修改 state 字段的状态来获取到锁。

Q:锁的可重入性是如何实现的?

A:当前线程发现 state 不是 0 ,则说明有锁已经被获取了,此时会判断当前获取到锁的线程是不是自己,如果是,则对 state 进行累加。

Q:当前线程获取锁失败,被阻塞的后续操作是什么?

A:获取失败,会放到 AQS 等待队列中,在队列中不断循环,监视前一个节点是否为 head ,是的话,会重新尝试获取锁。

Q:公平锁和非公平锁是如何体现的?

A:公平锁主要体现在如果当前队列中已经有排队的线程了,则自己直接排在后面。非公平锁是不管当前队列都没有线程排队,都会直接尝试修改 state 获取锁。

Q:锁是如何释放的?

A:锁释放资源,即将 state 进行 -1 操作,如果 -1 后 state 为 0,则释放节点,后续节点尝试获取锁。此处可以看 AQS 相关逻辑。

不能再被问住了!ReentrantLock 源码、画图一起看一看!的更多相关文章

  1. java多线程---ReentrantLock源码分析

    ReentrantLock源码分析 基础知识复习 synchronized和lock的区别 synchronized是非公平锁,无法保证线程按照申请锁的顺序获得锁,而Lock锁提供了可选参数,可以配置 ...

  2. JUC AQS ReentrantLock源码分析

    警告⚠️:本文耗时很长,先做好心理准备,建议PC端浏览器浏览效果更佳. Java的内置锁一直都是备受争议的,在JDK1.6之前,synchronized这个重量级锁其性能一直都是较为低下,虽然在1.6 ...

  3. ReentrantLock 源码分析从入门到入土

    回答一个问题 在开始本篇文章的内容讲述前,先来回答我一个问题,为什么 JDK 提供一个 synchronized 关键字之后还要提供一个 Lock 锁,这不是多此一举吗?难道 JDK 设计人员都是沙雕 ...

  4. Java并发系列[5]----ReentrantLock源码分析

    在Java5.0之前,协调对共享对象的访问可以使用的机制只有synchronized和volatile.我们知道synchronized关键字实现了内置锁,而volatile关键字保证了多线程的内存可 ...

  5. Java并发编程-ReentrantLock源码分析

    一.前言 在分析了 AbstractQueuedSynchronier 源码后,接着分析ReentrantLock源码,其实在 AbstractQueuedSynchronizer 的分析中,已经提到 ...

  6. ReentrantLock源码分析--jdk1.8

    JDK1.8 ArrayList源码分析--jdk1.8LinkedList源码分析--jdk1.8HashMap源码分析--jdk1.8AQS源码分析--jdk1.8ReentrantLock源码分 ...

  7. 死磕 java同步系列之ReentrantLock源码解析(二)——条件锁

    问题 (1)条件锁是什么? (2)条件锁适用于什么场景? (3)条件锁的await()是在其它线程signal()的时候唤醒的吗? 简介 条件锁,是指在获取锁之后发现当前业务场景自己无法处理,而需要等 ...

  8. java源码-ReentrantLock源码分析-1

    ReentrantLock 继承于lock是比较常用的独占锁,接下来我们来分析一下ReentrantLock源码以及接口设计: Sync是ReentrantLock的内部静态抽象类继承Abstract ...

  9. ReentrantLock 源码分析以及 AQS (一)

    前言 JDK1.5 之后发布了JUC(java.util.concurrent),用于解决多线程并发问题.AQS 是一个特别重要的同步框架,很多同步类都借助于 AQS 实现了对线程同步状态的管理. A ...

  10. ReentrantLock源码解析

    ReentrantLock 1 数据结构 从上图可以看出,ReentrantLock的功能都是通过sync这个对象提供的. public class ReentrantLock implements ...

随机推荐

  1. js undefined和null的判断

    function isUndef(value ){ return value === undefined || value === null; }

  2. 001 01 Android 零基础入门 01 Java基础语法 01 Java初识 01 导学

    001 01 Android 零基础入门 01 Java基础语法 01 Java初识 01 导学 welcome to Java World 欢迎来到Java世界 一起领略Java编程世界的奥秘与奥妙 ...

  3. Java (二)基于Eclipse配置Commons IO的环境

    上一篇:Java (一)下载APACHE Commons IO 一.新建一个Java Project 二.使用鼠标右键点击工程,选择New > Folder 三.在Folder name输入文件 ...

  4. 简单区间dp

    题目链接 对于基本区间dp,设dp[l][r]是区间l到r的最大价值. 我们可以枚举区间的长度,在枚举左端点,判断即可. 当右端点大于n,就break. dp[l][r]=max(dp[l+1][r] ...

  5. RHSA-2019:0201-低危: systemd 安全更新

    [root@localhost ~]# cat /etc/redhat-release CentOS Linux release 7.2.1511 (Core) 修复命令: 使用root账号登陆She ...

  6. 多测师讲解python _函数return_高级讲师肖sir

    # 函数中的返回的作用: 注意点:(1)调用函数===没有加print 调用函数为空,加了print调用函数打印输出none              (2)在函数中碰到return语句赋值直接返回r ...

  7. 用Docker容器安装Jenkins

    先安装Docker 可以参考我的上一篇文章 链接 拉取Jenkins最新镜像,可跟版本号 不跟默认拉取最新镜像 docker pull jenkins/jenkins 创建JenKins的工作目录 m ...

  8. MySQL数据库规范 (设计规范+设计规范+操作规范)

    I 文档定义 1.1 编写目的 为了在软件生命周期内规范数据库相关的需求分析.设计.开发.测试.运维工作,便于不同团队之间的沟通协调,以及在相关规范上达成共识,提升相关环节的工作效率和系统的可维护性. ...

  9. spring boot:用itextpdf处理pdf表格文件(spring boot 2.3.2)

    一,什么是itextpdf? 1,itextpdf的用途 itextpdf是用来生成PDF文档的一个java类库, 通过iText可以生成PDF文档, 还可以把XML/Html文件转化为PDF文件 2 ...

  10. laravel job 队列

    1.数据库建表 php artisan queue:table<span> </span>//队列任务表 php artisan queue:failed-table<s ...