java底层组合AQS实现类kReentrantLock源码解析

目录

不啰嗦,我们直接开始!

引导语

上两小节我们学习了 AQS,本章我们就要来学习一下第一个 AQS 的实现类:ReentrantLock,看看其底层是如何组合 AQS ,实现了自己的那些功能。

本章的描述思路是先描述清楚 ReentrantLock 的构成组件,然后使用加锁和释放锁的方法把这些组件串起来。

1、类注释

ReentrantLock 中文我们习惯叫做可重入互斥锁,可重入的意思是同一个线程可以对同一个共享资源重复的加锁或释放锁,互斥就是 AQS 中的排它锁的意思,只允许一个线程获得锁。

我们来一起来看下类注释上都有哪些重要信息:

可重入互斥锁,和 synchronized 锁具有同样的功能语义,但更有扩展性;构造器接受 fairness 的参数,fairness 是 ture 时,保证获得锁时的顺序,false 不保证;公平锁的吞吐量较低,获得锁的公平性不能代表线程调度的公平性;tryLock() 无参方法没有遵循公平性,是非公平的(lock 和 unlock 都有公平和非公平,而 tryLock 只有公平锁,所以单独拿出来说一说)。

我们补充一下第二点,ReentrantLock 的公平和非公平,是针对获得锁来说的,如果是公平的,可以保证同步队列中的线程从头到尾的顺序依次获得锁,非公平的就无法保证,在释放锁的过程中,我们是没有公平和非公平的说法的。

2、类结构

ReentrantLock 类本身是不继承 AQS 的,实现了 Lock 接口,如下:

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

Lock 接口定义了各种加锁,释放锁的方法,接口有如下几个:

// 获得锁方法,获取不到锁的线程会到同步队列中阻塞排队void lock();// 获取可中断的锁void lockInterruptibly() throws InterruptedException;// 尝试获得锁,如果锁空闲,立马返回 true,否则返回 falseboolean tryLock();// 带有超时等待时间的锁,如果超时时间到了,仍然没有获得锁,返回 falseboolean tryLock(long time, TimeUnit unit) throws InterruptedException;// 释放锁void unlock();// 得到新的 ConditionCondition newCondition();

ReentrantLock 就负责实现这些接口,我们使用时,直接面对的也是这些方法,这些方法的底层实现都是交给 Sync 内部类去实现的,Sync 类的定义如下:

abstract static class Sync extends AbstractQueuedSynchronizer {}

Sync 继承了 AbstractQueuedSynchronizer ,所以 Sync 就具有了锁的框架,根据 AQS 的框架,Sync 只需要实现 AQS 预留的几个方法即可,但 Sync 也只是实现了部分方法,还有一些交给子类 NonfairSync 和 FairSync 去实现了,NonfairSync 是非公平锁,FairSync 是公平锁,定义如下:

// 同步器 Sync 的两个子类锁static final class FairSync extends Sync {}static final class NonfairSync extends Sync {}

几个类整体的结构如下:

图中 Sync、NonfairSync、FairSync 都是静态内部类的方式实现的,这个也符合 AQS 框架定义的实现标准。

3、构造器 

ReentrantLock 构造器有两种,代码如下:

// 无参数构造器,相当于 ReentrantLock(false),默认是非公平的public ReentrantLock() {    sync = new NonfairSync();} public ReentrantLock(boolean fair) {    sync = fair ? new FairSync() : new NonfairSync();}

无参构造器默认构造是非公平的锁,有参构造器可以选择。

从构造器中可以看出,公平锁是依靠 FairSync 实现的,非公平锁是依靠 NonfairSync 实现的。

4、Sync 同步器

Sync 表示同步器,继承了 AQS,UML 图如下:

从 UML 图中可以看出,lock 方法是个抽象方法,留给 FairSync 和 NonfairSync 两个子类去实现,我们一起来看下剩余重要的几个方法。

4.1、nonfairTryAcquire 

// 尝试获得非公平锁final boolean nonfairTryAcquire(int acquires) {    final Thread current = Thread.currentThread();    int c = getState();    // 同步器的状态是 0,表示同步器的锁没有人持有    if (c == 0) {        // 当前线程持有锁        if (compareAndSetState(0, acquires)) {            // 标记当前持有锁的线程是谁            setExclusiveOwnerThread(current);            return true;        }    }    // 如果当前线程已经持有锁了,同一个线程可以对同一个资源重复加锁,代码实现的是可重入锁    else if (current == getExclusiveOwnerThread()) {        // 当前线程持有锁的数量 + acquires        int nextc = c + acquires;        // int 是有最大值的,<0 表示持有锁的数量超过了 int 的最大值        if (nextc < 0) // overflow            throw new Error("Maximum lock count exceeded");        setState(nextc);        return true;    }    //否则线程进入同步队列    return false;}

以上代码有三点需要注意:

通过判断 AQS 的 state 的状态来决定是否可以获得锁,0 表示锁是空闲的;else if 的代码体现了可重入加锁,同一个线程对共享资源重入加锁,底层实现就是把 state + 1,并且可重入的次数是有限制的,为 Integer 的最大值;这个方法是非公平的,所以只有非公平锁才会用到,公平锁是另外的实现。

无参的 tryLock 方法调用的就是此方法,tryLock 的方法源码如下:

public boolean tryLock() {    // 入参数是 1 表示尝试获得一次锁    return sync.nonfairTryAcquire(1);}

4.1、tryRelease

// 释放锁方法,非公平和公平锁都使用protected final boolean tryRelease(int releases) {    // 当前同步器的状态减去释放的个数,releases 一般为 1    int c = getState() - releases;    // 当前线程根本都不持有锁,报错    if (Thread.currentThread() != getExclusiveOwnerThread())        throw new IllegalMonitorStateException();    boolean free = false;    // 如果 c 为 0,表示当前线程持有的锁都释放了    if (c == 0) {        free = true;        setExclusiveOwnerThread(null);    }    // 如果 c 不为 0,那么就是可重入锁,并且锁没有释放完,用 state 减去 releases 即可,无需做其他操作    setState(c);    return free;}

tryRelease 方法是公平锁和非公平锁都公用的,在锁释放的时候,是没有公平和非公平的说法的。

从代码中可以看到,锁最终被释放的标椎是 state 的状态为 0,在重入加锁的情况下,需要重入解锁相应的次数后,才能最终把锁释放,比如线程 A 对共享资源 B 重入加锁 5 次,那么释放锁的话,也需要释放 5 次之后,才算真正的释放该共享资源了。

5、FairSync 公平锁

FairSync 公平锁只实现了 lock 和 tryAcquire 两个方法,lock 方法非常简单,如下:

// acquire 是 AQS 的方法,表示先尝试获得锁,失败之后进入同步队列阻塞等待final void lock() {    acquire(1);}

tryAcquire 方法是 AQS 在 acquire 方法中留给子类实现的抽象方法,FairSync 中实现的源码如下:

protected final boolean tryAcquire(int acquires) {    final Thread current = Thread.currentThread();    int c = getState();    if (c == 0) {        // hasQueuedPredecessors 是实现公平的关键        // 会判断当前线程是不是属于同步队列的头节点的下一个节点(头节点是释放锁的节点)        // 如果是(返回false),符合先进先出的原则,可以获得锁        // 如果不是(返回true),则继续等待        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;}

代码和 Sync 的 nonfairTryAcquire 方法实现类似,唯一不同的是在获得锁时使用 hasQueuedPredecessors 方法体现了其公平性。

6、NonfairSync 非公平锁

NonfairSync 底层实现了 lock 和 tryAcquire 两个方法,如下:

// 加锁final void lock() {    // cas 给 state 赋值    if (compareAndSetState(0, 1))        // cas 赋值成功,代表拿到当前锁,记录拿到锁的线程        setExclusiveOwnerThread(Thread.currentThread());    else        // acquire 是抽象类AQS的方法,        // 会再次尝试获得锁,失败会进入到同步队列中        acquire(1);}// 直接使用的是 Sync.nonfairTryAcquire 方法 protected final boolean tryAcquire(int acquires) {    return nonfairTryAcquire(acquires);}

7、如何串起来

以上内容主要说了 ReentrantLock 的基本结构,比较零散,那么这些零散的结构如何串联起来呢?我们是通过 lock、tryLock、unlock 这三个 API 将以上几个类串联起来,我们来一一看下。

7.1 lock 加锁

lock 的代码实现:

public void lock() {    sync.lock();}

其底层的调用关系(只是简单表明调用关系,并不是完整分支图)如下:

7.2 tryLock 尝试加锁 

 tryLock 有两个方法,一种是无参的,一种提供了超时时间的入参,两种内部是不同的实现机制,代码分别如下:

// 无参构造器public boolean tryLock() {    return sync.nonfairTryAcquire(1);}// timeout 为超时的时间,在时间内,仍没有得到锁,会返回 falsepublic boolean tryLock(long timeout, TimeUnit unit)        throws InterruptedException {    return sync.tryAcquireNanos(1, unit.toNanos(timeout));}

接着我们一起看下两个 tryLock 的调用关系图,下图显示的是无参 tryLock 的调用关系图,如下:

我们需要注意的是 tryLock 无参方法底层走的就是非公平锁实现,没有公平锁的实现。

下图展示的是带有超时时间的有参 tryLock 的调用实现图:

7.3、unlock 释放锁 

unlock 释放锁的方法,底层调用的是 Sync 同步器的 release 方法,release 是 AQS 的方法,分成两步:

尝试释放锁,如果释放失败,直接返回 false;释放成功,从同步队列的头节点的下一个节点开始唤醒,让其去竞争锁。

第一步就是我们上文中 Sync 的 tryRelease 方法(4.1),第二步 AQS 已经实现了。

unLock 的源码如下:

// 释放锁public void unlock() {    sync.release(1);}

7.4、Condition

ReentrantLock 对 Condition 并没有改造,直接使用 AQS 的 ConditionObject 即可。

8、总结

这就是我们在研究完 AQS 源码之后,碰到的第一个锁,是不是感觉很简单,AQS 搭建了整个锁架构,子类锁只需要根据场景,实现 AQS 对应的方法即可,不仅仅是 ReentrantLock 是这样,JUC 中的其它锁也都是这样,只要对 AQS 了如指掌,锁其实非常简单。

不啰嗦,文章结束,期待三连!

(0)

相关推荐

  • C++动态规划之背包问题解决方法

    本文实例讲述了C++动态规划之背包问题解决方法.分享给大家供大家参考.具体分析如下: 问题描述: 背包的最大容量为W,有N件物品,每件物品重量为w,价值为p,怎样选择物品能使得背包里的物品价值最大? 输入: 10 3   (W,N) 4 5   (w,p) 6 7   (w,p) 8 9   (w,p) 实现代码: #include <stdio.h> #define THING 20 #define WEIGHT 100 int arr[THING][WEIGHT]; /* 背包容量为wei

  • c++基础算法动态DP解决CoinChange问题

    目录 问题来源 问题简述 解决方案 真正的DP 补充--硬币不能重复使用 补充2--不同顺序表示不同组合 结束语 问题来源 这是Hackerrank上的一个比较有意思的问题,详见下面的链接: https://www.hackerrank.com/challenges/ctci-coin-change 问题简述 给定m个不同面额的硬币,C={c0, c1, c2-cm-1},找到共有几种不同的组合可以使得数额为n的钱换成等额的硬币(每种硬币可以重复使用). 比如:给定m=3,C={2,1,3},n

  • C语言使用DP动态规划思想解最大K乘积与乘积最大问题

    最大K乘积问题 设I是一个n位十进制整数.如果将I划分为k段,则可得到k个整数.这k个整数的乘积称为I的一个k乘积.试设计一个算法,对于给定的I和k,求出I的最大k乘积. 编程任务: 对于给定的I 和k,编程计算I 的最大k 乘积. 需求输入: 输入的第1 行中有2个正整数n和k.正整数n是序列的长度:正整数k是分割的段数.接下来的一行中是一个n位十进制整数.(n<=10) 需求输出: 计算出的最大k乘积. 解题思路:DP 设w(h,k) 表示: 从第1位到第K位所组成的十进制数,设m(i,j)

  • C语言矩阵连乘 (动态规划)详解

    动态规划法 题目描述:给定n个矩阵{A1,A2....An},其中Ai与Ai+1是可以相乘的,判断这n个矩阵通过加括号的方式相乘,使得相乘的次数最少! 以矩阵链ABCD为例 按照矩阵链长度递增计算最优值 矩阵链长度为1时,分别计算出矩阵链A.B.C.D的最优值 矩阵链长度为2时,分别计算出矩阵链AB.BC.CD的最优值 矩阵链长度为3时,分别计算出矩阵链ABC.BCD的最优值 矩阵链长度为4时,计算出矩阵链ABCD的最优值 动归方程: 分析: k为矩阵链断开的位置 d数组存放矩阵链计算的最优值,

  • java底层组合AQS实现类kReentrantLock源码解析

    目录 不啰嗦,我们直接开始! 引导语 上两小节我们学习了 AQS,本章我们就要来学习一下第一个 AQS 的实现类:ReentrantLock,看看其底层是如何组合 AQS ,实现了自己的那些功能. 本章的描述思路是先描述清楚 ReentrantLock 的构成组件,然后使用加锁和释放锁的方法把这些组件串起来. 1.类注释 ReentrantLock 中文我们习惯叫做可重入互斥锁,可重入的意思是同一个线程可以对同一个共享资源重复的加锁或释放锁,互斥就是 AQS 中的排它锁的意思,只允许一个线程获得

  • Android 中 SwipeLayout一个展示条目底层菜单的侧滑控件源码解析

    由于项目上的需要侧滑条目展示收藏按钮,记得之前代码家有写过一个厉害的开源控件 AndroidSwipeLayout 本来准备直接拿来使用,但是看过 issue 发现现在有不少使用者反应有不少的 bug ,而且代码家现在貌似也不进行维护了.故自己实现了一个所要效果的一个控件.因为只是实现我需要的效果,所以大家也能看到,代码里有不少地方我是写死的.希望对大家有些帮助.而且暂时也不需要 AndroidSwipeLayout 大而全的功能,算是变相给自己做的项目精简代码了. 完整示例代码请看:GitHu

  • Java中ArrayList类的源码解析

    前言:在前面我们提到数据结构的线性表.那么今天我们详细看下Java源码是如何实现线性表的,这一篇主要讲解顺序表ArrayList链式表下一篇在提及. 1:ArrayList结构图 2:关于Collection和List的区别 最好的比对就是查看他们的源码我们先看Collection的所有接口 public interface Collection<E> extends Iterable<E> { int size(); boolean contains(Object o); Ite

  • js继承 Base类的源码解析

    // timestamp: Tue, 01 May 2007 19:13:00 /* base2.js - copyright 2007, Dean Edwards http://www.opensource.org/licenses/mit-license */ // You know, writing a javascript library is awfully time consuming. //////////////////// BEGIN: CLOSURE ////////////

  • java底层AQS实现类kReentrantLock锁的构成及源码解析

    目录 引导语 1.类注释 2.类结构 3.构造器 4.Sync同步器 4.1.nonfairTryAcquire 4.2.tryRelease 5.FairSync公平锁 6.NonfairSync非公平锁 7.如何串起来 7.1lock加锁 7.2tryLock尝试加锁 7.3unlock释放锁 7.4Condition 8.总结 引导语 本章的描述思路是先描述清楚 ReentrantLock 的构成组件,然后使用加锁和释放锁的方法把这些组件串起来. 1.类注释 ReentrantLock 中

  • Java源码解析之object类

    在源码的阅读过程中,可以了解别人实现某个功能的涉及思路,看看他们是怎么想,怎么做的.接下来,我们看看这篇Java源码解析之object的详细内容. Java基类Object java.lang.Object,Java所有类的父类,在你编写一个类的时候,若无指定父类(没有显式extends一个父类)编译器(一般编译器完成该步骤)会默认的添加Object为该类的父类(可以将该类反编译看其字节码,不过貌似Java7自带的反编译javap现在看不到了). 再说的详细点:假如类A,没有显式继承其他类,编译

  • java.lang.Void类源码解析

    在一次源码查看ThreadGroup的时候,看到一段代码,为以下: /* * @throws NullPointerException if the parent argument is {@code null} * @throws SecurityException if the current thread cannot create a * thread in the specified thread group. */ private static Void checkParentAcc

  • 深入了解Java线程池:从设计思想到源码解读

    目录 为什么需要线程池 线程池设计思路 线程池的工作机制 线程池的参数及使用 线程池的状态 提交任务 任务队列 线程工厂 拒绝策略 关闭线程池 Executors 静态工厂 合理地配置线程池 线程池的监控 源码分析 execute addWorker Worker runWorker getTask processWorkerExit 面试题 为什么需要线程池 我们知道创建线程的常用方式就是 new Thread() ,而每一次 new Thread() 都会重新创建一个线程,而线程的创建和销毁

  • Java源码解析重写锁的设计结构和细节

    目录 引导语 1.需求 2.详细设计 2.1.定义锁 2.2.定义同步器Sync 2.3.通过能否获得锁来决定能否得到链接 3.测试 4.总结 引导语 有的面试官喜欢让同学在说完锁的原理之后,让你重写一个新的锁,要求现场在白板上写出大概的思路和代码逻辑,这种面试题目,蛮难的,我个人觉得其侧重点主要是两个部分: 考察一下你对锁原理的理解是如何来的,如果你对源码没有解读过的话,只是看看网上的文章,或者背面试题,也是能够说出大概的原理,但你很难现场写出一个锁的实现代码,除非你真的看过源码,或者有和锁相

  • Java 线程池ThreadPoolExecutor源码解析

    目录 引导语 1.整体架构图 1.1.类结构 1.2.类注释 1.3.ThreadPoolExecutor重要属性 2.线程池的任务提交 3.线程执行完任务之后都在干啥 4.总结 引导语 线程池我们在工作中经常会用到.在请求量大时,使用线程池,可以充分利用机器资源,增加请求的处理速度,本章节我们就和大家一起来学习线程池. 本章的顺序,先说源码,弄懂原理,接着看一看面试题,最后看看实际工作中是如何运用线程池的. 1.整体架构图 我们画了线程池的整体图,如下: 本小节主要就按照这个图来进行 Thre

随机推荐