前言

在阅读完 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. matlab中find 查找非零元素的索引和值

    来源:https://ww2.mathworks.cn/help/matlab/ref/find.html?searchHighlight=find&s_tid=doc_srchtitle f ...

  2. 【学习笔记/题解】分层图/[JLOI2011]飞行路线

    题目戳我 \(\text{Solution:}\) 关于分层图: 一般用于处理:给你\(k\)次机会对边权进行修改的最短路问题. 算法流程: 建立出\(k\)层图,对应进行\(k\)次操作后的局面. ...

  3. [C#.NET 拾遗补漏]09:数据标注与数据校验

    数据标注(Data Annotation)是类或类成员添加上下文信息的一种方式,在 C# 通常用特性(Attribute)类来描述.它的用途主要可以分为下面这三类: 验证 Validation:向数据 ...

  4. SpringBoot-06-模板引擎Thymeleaf

    6. 模板引擎 Thymeleaf Thyme leaf 英译为 百里香的叶子. 模板引擎 ​ 以前开发中使用的jsp就是一个模板引擎,但是springboot 以jar的方式,并且使用嵌入式的tom ...

  5. dockerfile镜像设置中文

    一.dockerfile镜像设置中文 centos镜像默认不支持中文,把下面的内容加到dockerfile即可 # 修改时区 RUN rm -rf /etc/localtime && ...

  6. Microsoft.VisualBasic.dll内置的判断变量类型的一系列实用方法

    今天意外读到一线码农的一篇文章<挖一挖C#中那些我们不常用的东西之系列(2)--IsXXX 系列方法>,文章中讲到 Microsoft.VisualBasic.dll 里面的Informa ...

  7. Linux执行脚本让进程挂掉后自动重启

    1 创建循环监听脚本  autostart.sh 例: 其中futures-market-server-v3andwebsoket.jar 是要监听的执行程序 #/bin/bashwhile true ...

  8. day53 Pyhton 前端04

    内容回顾: 盒子: 内边距:padding,解决内部矛盾,内边距的增加整个盒子也会增加 外边距:margin,解决外部矛盾,当来盒子都有外边距的时候,取两者最大值 边框:border border-c ...

  9. npoi 设置单元格格式

    using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...

  10. vim插件配置

    OS:kali linux tool:vim 上图: 0x00 需要用到的插件及其下载地址 左边的一栏显示文件目录结构的用到的插件为 NERDTree 下载地址:https://github.com/ ...