JDK8 源码解读:ReentrantLock - 核心方法

前言

关于变量的说明,请看:JDK8 源码解读:ReentrantLock - 变量与结构

由于 ReentrantLock 在公平锁与非公平锁代码层面上的差别并不大,因此我们先从最常用的非公平锁入手,最后来看和公平锁的差异

LockSupport

LockSupport 可以理解为一个工具类,也是 AQS 里的一个核心。它的作用很简单,就是挂起和继续执行线程。

  • public static void park() : 如果没有可用许可,则挂起当前线程,因此如果已经有许可就能直接走
  • public static void unpark(Thread thread): 给 thread 一个可用的许可,让它得以继续执行

如何理解许可这个概念

说简单点就相当于一个买票,你可以上车前买票,也可以下车时候补票,没票就不让你出去

每个线程初始时是没有许可的,只有执行 unpark 时候,才会给指定线程释放一个许可

例子1:
可以看到,先执行 unpark,在执行 park,线程能正常往下走,因为先执行 unpark 相当于已经给线程准备好了许可证,park 时候直接就获取到许可了,能直接走
在这里插入图片描述
例子2:
两个线程,由 t1 线程去获取许可,再由 t2 线程去给 t1 线程释放许可。

可以看到,虽然 t2 线程睡眠了一秒,但是 t1 线程永远在 t2 线程之后结束,因为 t1 需要等 t2 赋予许可。
在这里插入图片描述

非公平锁

无参构造函数

  • NonfairSync: 非公平锁对象
  • FairSync: 公平锁对象

可以看到通过无参构造函数,会初始化非公平锁的同步器,这个类继承了 Sync 同步器,因此也相当于继承了 AQS

public ReentrantLock() {
    sync = new NonfairSync();
}

在这里插入图片描述

lock()

final void lock()

调用 lock 方法的时候,由于我们初始化的是 NonfairSync 因此抽象方法会指向 NonfairSync 里的 lock 方法
在这里插入图片描述
先来看 compareAndSetState 方法,这个方法是将 AQSstate 字段在当前值与期望值相同都为 0 的前提下,更新为 1,注意不会进行失败重试(可以把这个方法里的英语翻译下)。

上篇文章有说,CAS 是为了保证原子操作,state 是类似持锁状态的字段,因此有线程将值变为 1 后,其他线程就不能直接获取到锁.

至于 setExclusiveOwnerThread 方法很简单,就是设置当前独占线程,不多说。

小总结:从目前的逻辑来看很简单,第一个获取锁的线程会因为 CAS 操作成功,直接进入 setExclusiveOwnerThread 设置独占线程。后续的线程,不管是不是重入,都会进入 acquire 尝试进行许可的获取。同时我们也会发现,第一个线程进入时候,AQS 中的参数我们都没有进行初始化。

进入 acquire 方法,参数为 1,因为我们是独占锁,因此只要一个许可

就目前从 if 判断中,我们大致能知道,这里做了什么事

首先尝试获取许可,如果失败就将节点加入等待队列
在这里插入图片描述

下面我们来看看核心方法

protected final boolean tryAcquire(int acquires)

如果直接点进去 tryAcquire 我们会发现,它啥事没干直接抛出了一个 UnsupportedOperationException 异常,因为这个方法本质是给子类重写的。

由于 ReentrantLockSync 继承了 AQS,而非公平锁对象 NonfairSync 又继承了 Sync。根据子类继承父类进行方法重写能够覆盖父类的原方法,因此我们真正调用的是 NonfairSync 里的 tryAcquire
在这里插入图片描述
继续往里进
在这里插入图片描述
之前说过,同步状态 state0 时,表示没有线程进入同步状态,也就是持锁,所以先进性一次这个判断。

if(c == 0) 为 true 时,执行的部分代码与前面 lock 里的可以说是如出一辙,原因是防止这过程中有线程释放锁了,这时候就直接拿锁,因此我们来看 else 部分的代码。

首先判断是否是当前线程,是的话进行重入

变量的文章中已经说过 state 字段表示,是否已经加锁的同时,同时也表示了线程的重入次数,因此 int nextc = c + acquires 就是对重入次数的累加,完成后直接返回 true,重入执行完成。

但是如果不是重入,就还要有后续的操作,此时会返回 false。

private Node addWaiter(Node mode)

如果不是重入节点,此时我们就需要将节点加入同步等待队列
在这里插入图片描述
首先判断尾结点不为空,这个主要针对同步等待队列已经存在的情况,此时就直接 CAS 加入到队列中。我们知道 CAS 是可能会失败的,因此此处只是为了一个提升性能的尝试,是在失败了,就进 enq 再说。
在这里插入图片描述
可以看到,enq 中有个 for 循环会进行无限次的重试,因此不怕失败。

if 判断的下半部很眼熟,我们跳过,看上半部分。

此处判断了尾结点为空,我们知道只有当等待队列不存在时,尾结点才为 null,因此需要构建同步等待队列

注意,这里 CAS 构建头结点用的是 new Node()。变量篇中的注释有写过,等待队列的头结点表示正在运行的线程节点,因此这里 new Node() 作为头结点,很简单起到了占位作用,至于当前节点会在下次 for 循环里进入 else。

下面,添加完节点,我们就要进入锁的核心,也就是锁为什么能阻塞线程,进入 acquireQueued 方法。

final boolean acquireQueued(final Node node, int arg)

传入当前节点 node
在这里插入图片描述
可以看到一个 for 死循环,没有拿到许可的线程本身出不了这个循环的基础上,还会被 parkAndCheckInterrupt 操作中的 park 给阻塞住,直到拿到许可。
在这里插入图片描述
看这截代码,前提是前驱节点是头节点,要不然就只能继续排队。之后尝试获取许可。

如果成功了,可以看到,将当期节点设置成头结点 (头结点表示正在运行的线程的节点!!!),之后返回中断状态

如果并不能拿到许可,走下面,先来看看 shouldParkAfterFailedAcquire 方法
在这里插入图片描述
这步的核心作用是对等待队列进行数据的清洗。

  • 如果前驱节点状态为待唤醒,直接返回 true。
  • 如果前驱节点状态为取消,此时就会进行递归,从前驱节点的 pred 属性一直向前找,知道找到一个状态不是取消状态的,把中间这部分取消状态的节点,从链表中移除。然后返回 false。
  • 如果前驱状态状态为初始状态,变为待唤醒状态后,返回 false,等待下轮循环。

在这里插入图片描述

假如 shouldParkAfterFailedAcquire 返回 true,parkAndCheckInterrupt 方法会对当前线程进行 park 也就是阻塞操作,直到当前线程拿到许可,才会返回线程的中断状态。
在这里插入图片描述
最后 acquire 中的 selfInterrupt 会对本处于本处于中断状态的线程进行一个中断状态恢复。会发现 ReentrantLock 是先做了一次中断状态取消,再恢复,个人认为这是保证一个逻辑的严谨与正确性

正常的话,加锁过程到这里就结束了。

private void cancelAcquire(Node node)

假如在 acquireQueued 中抛异常了,就会被 catch 进入取消正在获取许可的操作
在这里插入图片描述
刚开始的操作并不复杂,无非就是先进行了当前节点之前的取消节点的清理,将一些参数设置为 null 便于 GC,然后把当前线程的节点设置为取消状态

直接进入 if 判断部分,上半部分也很好理解,如果自己是尾结点,将前驱节点变为尾结点,因为做了节点清洗,因此大不了 tail == head

看下半部分,如果前驱节点不是头结点,强制修改状态为待唤醒,然后将自己从链表上移除。反之如果前驱节点是头结点,unparkSuccessor 就对下个节点进行唤醒。

private void unparkSuccessor(Node node)

在这里插入图片描述
可以看到 unparkSuccessor 方法中,直接将 state 方法字段改为 0 并且找到下个可用的线程,直接给予了线程许可。

PS:说实话,我也不是太明白,什么情况下才会出异常进入这里,这里是如何保证 Head 节点已经执行完了,才会直接修改了同步状态,并且直接给了下个节点许可。毕竟抛异常的并不是 Head 节点!!!!

但是这个方法在 unlock 时候也会被调用,那时候是没有问题的。

unlock()

我们来看看 unlock() 解锁过程都做了什么,说实话比较简单。
在这里插入图片描述
依然是独占锁的原,因此取消许可的数量依然是 1

protected final boolean tryRelease(int releases)

尝试进行解锁,同样的,这个方法也是为了给子类重写的,因此我们可以从 ReentrantLock 的 Sync 中找到 tryRelease 的具体实现
在这里插入图片描述
逻辑很简单,也很好理解。

首先递减重入次数,然后判断独占线程是否为当前线程,不是抛异常,谁加锁的谁解锁。假如重入次数减到 0 了,说明线程完全解锁了,因此清空独占线程,重置同步状态,返回是否成功完全解锁。
在这里插入图片描述
如果成功了,就会进入 unparkSuccessor 唤醒下一个线程节点,让它拿锁。

至此,解锁结束,很简单。

公平锁

讲完非公平锁,来看看公平锁,并且比较下两者的差异,是如何实现公平的。

构造函数

同理,先来看看构造函数
在这里插入图片描述
可以看到,如果传入的是 true,就会构建 FairSync 属于公平锁的同步器,它同样继承了 AQS

与非公平锁的差异

差异一:lock() 方法差异

非公平锁:
在这里插入图片描述
公平锁:
在这里插入图片描述
可以看到,差异非常明显。非公平锁处要多了一个 if else

举例:
假如说我已经存在同步队列了。此时一个新线程刚刚进行加锁走到此处的 if,而同步队列中正好还没走到 tryAcquire 处,此时由于上个线程已经释放了许可,state 字段已经被置为 0,此时这个新线程就会很顺理成章的拿到独占锁。

可以说这个概率并不低,因为同步队列中做的事情并不少,比如循环处理取消节点等。

差异二:tryAcquire(int acquires) 方法差异

非公平锁:
在这里插入图片描述
公平锁:
在这里插入图片描述
可以看到公平锁比非公平锁多执行了一个 !hasQueuedPredecessors() 方法,来看看方法

在这里插入图片描述
这段代码的核心,其实就是 s.thread != Thread.currentThread() 校验。

举例:
同样是新线程与同步等待队列中的节点间的竞争

假如锁释放是在新线程走完 lock 的判断后在 tryAcquirec == 0 的判断之前完成释放,就会在此处 tryAcquire 处再次竞争。但是由于公平锁 hasQueuedPredecessors 中对当前线程的校验,此时新线程由于不是当前线程(如果是当前线程就是重入锁,此时持锁的就是自己,c 就不可能为 0),因此新线程就只能乖乖去等待队列中排队,无法竞争。

结尾

JDK8 源码解读:ReentrantLock - 变量与结构

源码

JDK 1.8 源码阅读-注释版

JDK 1.8 源码阅读

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值