并发——深入分析ReentrantLock的实现原理

1|0一、前言


  之前花了点时间研究了一下并发包下的一个重要组件——抽象队列同步器AQS,在并发包中,很多的类都是基于它实现的,包括Java中常用的锁ReentrantLock。知晓了AQS的实现原理,那理解ReentrantLock的实现就非常简单了,因为它的锁功能的实现就是由AQS实现的,而它的工作仅仅是重写了一些AQS中的相关方法,并使用其中的模板方法进行加锁解锁。今天这篇博客就来从源码的角度分析一下ReentrantLock的实现。


2|0二、正文


2|12.1 抽象队列同步器AQS


  在说ReentrantLock前,必须要先提一下AQSAQS全称抽象队列同步器(AbstractQuenedSynchronizer),它是一个可以用来实现线程同步的基础框架。当然,它不是我们理解的Spring这种框架,它是一个类,类名就是AbstractQuenedSynchronizer,如果我们想要实现一个能够完成线程同步的锁或者类似的同步组件,就可以在使用AQS来实现,因为它封装了线程同步的方式,我们在自己的类中使用它,就可以很方便的实现一个我们自己的锁。

  AQS的实现相对复杂,无法通过短短的几句话将其说清楚,我之前专门写过一篇分析AQS实现原理的博客:并发——抽象队列同步器AQS的实现原理

  在阅读下面的内容前,请一定要先学习AQS的实现原理,因为ReentrantLock的实现非常简单,完全就是依赖于AQS的,所以我以下的描述均建立在已经理解AQS的基础之上。可以阅读上面推荐博客,也可以自己去查阅相关资料。


2|22.2 ReentrantLock的实现原理


  我们先简单介绍一下ReentrantLock的实现原理,这样方便我们下面阅读它的源码。前面也说过,ReentrantLock基于AQS实现,AQS模板方法acquirerelease等,已经实现了加锁和解锁的操作,而使用它的类只需要重写这些模板方法中调用的方法,比如tryAcquiretryRelease等,这些方法通过修改AQS的同步状态state来加锁解锁。AQS的同步状态state是一个int类型的值,根据不同的值,就可以判断当前锁的状态,同时修改这个值就是加锁和解锁的方式。

  使用AQS的一般方式是以内部类的形式继承AQSReentrantLock也是这么实现的,在它的内部,有三个AQS的派生类:

  1. 首先第一个派生类名字叫做Sync,这是一个抽象类,直接继承自AQS,其中定义了一些通用的方法;
  2. 第二个派生类名字叫做NonfairSync,它继承自Sync,实现的是一种非公平锁
  3. 第三个派生类名字叫FairSync,它也继承自Sync,实现的是一种公平锁

  ReentrantLock就是通过NonfairSync对象或者FairSync对象来保证进行线程同步的。而这三个类中编写的方法,实际上就是修改同步状态的方式。当state的值为0时,表示当前并没有线程获取锁,而每获取一次锁,state的值就会+1,释放一次锁,state-1。下面我们就通过这三个类的源码来具体看一看吧。


2|32.3 Sync类源码解析


  我们直接来看看Sync类中的方法吧,Sync类中的方法不少,我只拿出其中比较重要的几个来讲一讲:

abstract static class Sync extends AbstractQueuedSynchronizer { /** 定义一个加锁的抽象方法,由子类实现 */ abstract void lock(); /** * 此方法的作用是以非公平的方式尝试获取一次锁,获取成功则返回true,否则返回false; * 需要注意,AQS的获取锁,实际上就是修改同步状态state的值。 * 这里有个疑惑,既然是非公平地获取锁,那这个方法为什么不写在NonfairSync类中? * 因为ReentrantLock有一个方法tryLock,即尝试获取一次锁,调用tryLock方法时, * 无论使用的是公平锁还是非公平锁,实际上都需要尝试获取一次锁,也就是调用这个方法, * 所以这个方法定义在了父类Sync中 */ final boolean nonfairTryAcquire(int acquires) { // 获取当前正在运行的线程 final Thread current = Thread.currentThread(); // 获取同步状态state的值,state定义在父类AQS中, int c = getState(); // 若当前state的值为0,表示还没有线程获取锁,于是当前线程可以尝试获取锁 if (c == 0) { // compareAndSetState方法通过CAS的方式修改state的值, // 实际上就是让state从0变为1,因为acquires的值就是1, // 每次有线程获取了锁时,同步状态就+1 if (compareAndSetState(0, acquires)) { // 若compareAndSetState方法返回true,表示修改state成功, // 则调用setExclusiveOwnerThread方法将当前线程记录为占用锁的线程 setExclusiveOwnerThread(current); return true; } } // 若以上c == 0不满足,则表示已经有线程获取锁了, // 于是调用getExclusiveOwnerThread方法获取当前正在占用锁的线程, // 然后和当前线程比较,若当前线程就是占用锁的线程,则当前线程不会被阻塞, // 可以再次获取锁,从这里可以看出,ReentrantLock是一个可重入锁 else if (current == getExclusiveOwnerThread()) { // 计算当前线程获取锁后,state的值应该是多少,实际上就是让state + 1 int nextc = c + acquires; // 如果nextc小于0,则保存,因为理论上同步状态是不可能小于0的 if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); // 使用上面计算出的nextc更新state的值,这里需要注意一点 // setState不像compareAndSetState方法,setState方法并不保证操作的原子性 // 这里不需要保证原子性,因为这里线程已经获取了锁,所以不会有其他线程进行操作 setState(nextc); // 返回true表示加锁成功 return true; } // 若以上条件均不满足,表示有其他线程获取了锁,当前线程获取锁失败 return false; } /** * 此方法是的作用是尝试释放锁,其实也就是让state的值-1 * 这个方法是一个通用的方法,不论使用的是公平锁还是非公平锁 * 释放锁时都是调用此方法修改同步状态 */ protected final boolean tryRelease(int releases) { // getState方法获取state的值,并与参数相减,计算释放锁后state的值 // 在ReentrantLock中其实就是-1 int c = getState() - releases; // 判断当前线程是不是占用锁的线程,若不是则抛出异常 // 因为只有占用了锁的线程才可以释放锁 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); // 变量free用来标记锁释放真正的被释放,因为ReentranLock是一个重入锁 // 获取锁的线程可以多次获取锁,只有每一次获取都释放,锁才是真正的释放 boolean free = false; // 判断c的值是否是0,只有c的值是0,也就是state的值为0时 // 才说明当前的线程在这次释放锁后,锁真正的处于没有被使用的状态 if (c == 0) { // 若满足此条件,则free标记为true,表示锁真的被释放了 free = true; // 然后标记当前占用锁的线程为null,也就是没有线程占用锁 setExclusiveOwnerThread(null); } // 将c的值更新同步状态state setState(c); return free; } /** 此方法判断当前线程是不是获取锁的线程 */ protected final boolean isHeldExclusively() { // getExclusiveOwnerThread方法返回当前正在占用锁的线程 // 于当前的运行的线程进行比较 return getExclusiveOwnerThread() == Thread.currentThread(); } }

  以上就是Sync类的实现。其实Sync中的方法不仅仅只有上面这几个,但是剩下的那些方法都是一些零零碎碎,对我们理解ReentrantLock没有太大帮助的方法,所以这里就不一一列举了。从上面的方法实现中,我们可以知道以下信息:线程获取锁的方式实际上就是让同步状态state的值增加,而释放锁的方式就是让state的值减小;而且ReentrantLock实现的是可重入锁,已经获取锁的线程可以不受阻碍地再次获取锁,state的值可以不断增加,而释放锁时,只有state的值减小为0,锁才是真正被释放


2|42.4 NonfairSync类源码解析


下面我们再看看第二个内部类NonfairSync,它实现的是非公平锁

/** * 此类继承自Sync,它实现的是非公平锁 */ static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * 在父类Sync中定义的lock方法,在子类中实现 */ final void lock() { // 调用compareAndSetState方法,企图使用CAS机制将state的值从0修改为1 // 若state的值不为0,表示锁已经被其他线程获取了,则当前线程将获取锁失败 // 或者state的值一开始是0,但是在当前线程修改的过程中,被其他线程修改了, // 也会返回false。若修改state失败,则就需要执行下面的acquire方法 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); // acquire方法是AQS定义的模板方法,这个方法会调用tryAcquire尝试获取锁, // 而tryAcquire方法是由子类实现的,也就是下面那个方法; // 若调用tryAcquire获取锁失败,则AQS会将线程封装成一个节点Node // 丢入AQS的同步队列中排队(这个具体实现请参考AQS的实现博客) // 归根到底,这个方法就是让线程获取锁,不断尝试,直到成功为止. // 注意这里传入的参数是1,表示加锁实际上就是让state的值+1 else acquire(1); } /** * 此方法tryAcquire在AQS的模板方法中被调用,它的作用就是尝试获取一次锁, * 也就是尝试修改一次同步状态state; * 不同的实现类根据不同的需求重写tryAcquire方法,就可以按自己的意愿控制加锁的方式 * AQS就是通过这种方式来提供给其他类使用的 */ protected final boolean tryAcquire(int acquires) { // 此处直接调用了父类Sync中,非公平地获取一次锁的nonfairTryAcquire方法 return nonfairTryAcquire(acquires); } }

  上面就是NonfairSync类完整的代码,并没有删减,可以看出,非常的简短。实现了Sync类中定义的lock方法,同时重写了tryAcquire方法,供AQS的模板方法acquire调用,且tryAcquire的实现仅仅是调用了Sync中的nonfairTryAcquire方法。为了有助于我们理解,我们还是来看看AQSacquire方法的代码吧:

public final void acquire(int arg) { // 这里首先调用tryAcquire方法尝试获取一次锁,在AQS中这个方法没有实现, // 而具体实现是在子类中,也就是调用的是NonfairSync的tryAcquire方法, // 若方法返回true,表示成功获取到锁,于是后面代码都不会执行了, // 否则,将先执行addWaiter方法,这个方法的作用是将当前线程封装成为一个Node节点, // 加入到AQS的同步队列的尾部,同时将返回这个Node节点,并传入acquireQueued方法 // acquireQueued方法的作用就是让当前线程阻塞,直到成功获取到锁才会从这个方法返回 // acquireQueued会返回这个线程在等待的过程中是否被中断,若被中断, // 则调用selfInterrupt方法真正执行中断。 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }

  为什么说NonfairSync是非公平锁?我们可以看到,在NonfairSynclock方法中,一个线程尝试去获取锁前,并不会判断在它之前是否有线程正在等待获取锁,而是直接尝试调用compareAndSetState方法获取一次锁,若获取失败,进入acquire方法,在这个方法中又会调用tryAcquire方法获取一次锁。此时若再次获取失败,才会进行进入同步队列中排队,这个过程中插了两次队,所以NonfairSync是非公平锁。


2|52.5 FairSync类源码解析


  下面我们来看看最后一个内部类FairSync,它实现的是公平锁,也就是线程按照先来后到的顺序获取锁,而不会插队:

static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; /** 实现父类的lock方法,加锁 */ final void lock() { // 直接调用AQS的模板方法acquire进行加锁,调用这个方法后,线程若没有获取锁 // 则会被阻塞,直到获取了锁后才会返回。这里需要注意一点,和NonfairSync中的lock不同 // 这里直接调用acquire,而不会先调用一次compareAndSetState方法获取锁 // 因为FairSync是公平锁,所以不会执行这种插队的操作. // 注意这里传入的参数是1,表示加锁实际上就是让state的值+1 acquire(1); } /** * 和NonfairSync一样,重写AQS的tryAcquire方法,若使用的是FairSync, * 则acquire中将调用此tryAcquire方法,尝试获取一次锁 */ protected final boolean tryAcquire(int acquires) { // 首先获取当前正在执行的线程 final Thread current = Thread.currentThread(); // 记录同步状态 int c = getState(); // 若state的值为0,表示现在没有线程占用了锁,于是当前线程可以尝试获取锁 if (c == 0) { // 尝试获取锁前,先调用hasQueuedPredecessors方法,这个方法是判断 // 是否有其他线程正在排队尝试获取锁,若有,方法将返回true,那为了公平性, // 当前线程不能获取锁,于是直接结束,否则调用compareAndSetState修改state // 若修改成功,调用setExclusiveOwnerThread方法将自己设置为当前占用锁的线程 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } // 若state不等于0,则表示当前锁已经被线程占用,那此处判断占用锁的线程是否是自己 // 若是,则当前线程可以再次获取锁,因为ReentrantLock实现的是可重入锁, else if (current == getExclusiveOwnerThread()) { // 计算当前线程再次获取锁后,state的值将变为多少,此处实际上就是 + 1 int nextc = c + acquires; // 理论上state的值不可能小于0,于是若小于0,就报错 if (nextc < 0) throw new Error("Maximum lock count exceeded"); // 修改state的值为上面计算的新值,此处不需要CAS操作保证原子性, // 因为当前线程已经获取了锁,那其他线程就不能修改state,所以这里可以放心修改 setState(nextc); return true; } // 若以上条件均不满足,表示有其他线程占用了锁,则直接返回false return false; } }

  FairSync的实现也比较简单。值得注意的是,因为FairSync实现的是公平锁,所以线程获取锁前,会先判断是否有在它之前尝试获取锁的线程在排队,若有,则当前线程不能插队,也需要进行排队,并且排在那些线程之后


2|62.6 ReentrantLock的成员属性与构造方法


  看完了内部类,下面就正式来看一看ReentrantLock是如何操作的吧,首先看一看它的成员属性和构造方法构造方法:

/** 记录使用的锁对象 */ private final Sync sync; /** 默认构造方法,初始化锁对象,默认使用非公平锁 */ public ReentrantLock() { sync = new NonfairSync(); } /** 参数为boolean类型的构造方法,若为false,使用非公平锁,否则使用公平锁 */ public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }

2|72.7 ReentrantLock的加锁与解锁


  下面我就来看看ReentrantLock最重要的两个操作,加锁和解锁。

(1)获取锁的方法实现

/** * 此方法让当前线程获取锁,若获取失败,线程将阻塞,直到获取成功为止 * 此方法不会响应中断,也就是在没有获取锁前,将无法退出 */ public void lock() { // 直接调用锁对象的lock方法,也就是之前分析的内部类中的lock方法 sync.lock(); } /** * 此方法获取锁,和上面的方法类似,唯一的不同就是,调用这个方法获取锁时, * 若线程被阻塞,可以响应中断 */ public void lockInterruptibly() throws InterruptedException { // 调用sync对象的acquireInterruptibly方法,这个方法定义在AQS中, // 也是AQS提供给子类的一个模板方法,内部也是通过tryAcquire获取锁, // 若获取失败,线程将被阻塞,但是此方法会检测中断信号, // 若检测到中断,将通过抛出异常的方式退出阻塞 // 关于这个方法的具体实现,可以去参考AQS的相关博客,此处就不展开描述了 sync.acquireInterruptibly(1); } /** * 调用此方法尝试获取一次锁,不论成功失败,都会直接返回 */ public boolean tryLock() { // 此处直接调用Sync类中的nonfairTryAcquire方法, // 这也就是为什么nonfairTryAcquire定义在父类Sync中, // 因为不论是使用公平锁还是非公平锁,都需要在此处调用这个方法 return sync.nonfairTryAcquire(1); }

(2)是否锁的方法实现

/** * 此方法用来释放锁 */ public void unlock() { // 此处调用的是AQS的release方法,这个方法也是AQS提供的一个模板方法, // 在这个方法中,将调用子类重写的tryRelease方法尝试释放锁,若释放成功 // 则会唤醒等待队列中的下一个线程,让它停止阻塞,开始尝试获取锁, // 关于这个方法的具体实现,可以参考我之前推荐的AQS源码分析博客。 // 这里需要注意,传入的参数是1,表明释放锁实际上就是让state的值-1 sync.release(1); }

  以上就是ReentrantLock加锁和解锁的方法,出乎意料,非常的简单,每个方法都只有一句代码,调用AQS类中提供的模板方法。这就是AQS的好处,AQS封装了线程同步的代码,我们只需要在类中使用它,就能很简单的实现一个锁。所以我前面才说,在看ReentrantLock前,一定要先学习AQS,理解了AQS,理解ReentrantLock就完全没有难度了。

  上面这些就是ReentrantLock中的关键方法,其实除了这些方法之外,还有许多其他的方法,但是那些方法并不是关键,实现也都非常简单,基本上就是一句代码,可以自己直接去阅读源码,我这里就不一一列举了。


3|0三、总结


  经过上面的分析,我们会发现,ReentrantLock的实现原理非常的简单,因为它是基于AQS实现的,复杂性都被封装在了AQS中,ReentrantLock仅仅是它的使用者,所以,学习ReentrantLock实际上就是学习AQSAQSJava并发中的重要组件,很多的类都是基于它实现的,比如非常常用的CountDownLatchAQS也是面试中的常考题,所以一定要好好研究。此处再次推荐我写的AQS解析博客:并发——抽象队列同步器AQS的实现原理


4|0四、参考


  • JDK1.8源码

__EOF__

本文作者特务依昂
本文链接https://www.cnblogs.com/tuyang1129/p/12689122.html
关于博主:在互联网洋流中垂死挣扎,但依旧乐观的Java小菜鸟一枚!
版权声明:转载博客请注明出处,并附上原文链接!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   特务依昂  阅读(702)  评论(0编辑  收藏  举报
编辑推荐:
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示