JDK8 源码解读:ReentrantLock - 核心方法
JDK8 源码解读:ReentrantLock - 核心方法
- 前言
- LockSupport
- 非公平锁
- 无参构造函数
- lock()
- final void lock()
- protected final boolean tryAcquire(int acquires)
- private Node addWaiter(Node mode)
- final boolean acquireQueued(final Node node, int arg)
- private void cancelAcquire(Node node)
- private void unparkSuccessor(Node node)
- unlock()
- protected final boolean tryRelease(int releases)
- 公平锁
- 构造函数
- 与非公平锁的差异
- 差异一:lock() 方法差异
- 差异二:tryAcquire(int acquires) 方法差异
- 结尾
- 源码
前言
关于变量的说明,请看: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
方法,这个方法是将 AQS
中 state
字段在当前值与期望值相同都为 0
的前提下,更新为 1
,注意不会进行失败重试(可以把这个方法里的英语翻译下)。
上篇文章有说,CAS
是为了保证原子操作,state
是类似持锁状态的字段,因此有线程将值变为 1
后,其他线程就不能直接获取到锁.
至于 setExclusiveOwnerThread
方法很简单,就是设置当前独占线程,不多说。
小总结:从目前的逻辑来看很简单,第一个获取锁的线程会因为 CAS 操作成功,直接进入 setExclusiveOwnerThread 设置独占线程。后续的线程,不管是不是重入,都会进入 acquire 尝试进行许可的获取。同时我们也会发现,第一个线程进入时候,AQS 中的参数我们都没有进行初始化。
进入 acquire
方法,参数为 1,因为我们是独占锁,因此只要一个许可
就目前从 if 判断中,我们大致能知道,这里做了什么事
首先尝试获取许可,如果失败就将节点加入等待队列
下面我们来看看核心方法
protected final boolean tryAcquire(int acquires)
如果直接点进去 tryAcquire
我们会发现,它啥事没干直接抛出了一个 UnsupportedOperationException
异常,因为这个方法本质是给子类重写的。
由于 ReentrantLock
的 Sync
继承了 AQS
,而非公平锁对象 NonfairSync
又继承了 Sync
。根据子类继承父类进行方法重写能够覆盖父类的原方法,因此我们真正调用的是 NonfairSync
里的 tryAcquire
继续往里进
之前说过,同步状态 state
为 0
时,表示没有线程进入同步状态,也就是持锁,所以先进性一次这个判断。
当 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 的判断后在 tryAcquire
的 c == 0
的判断之前完成释放,就会在此处 tryAcquire 处再次竞争。但是由于公平锁 hasQueuedPredecessors
中对当前线程的校验,此时新线程由于不是当前线程(如果是当前线程就是重入锁,此时持锁的就是自己,c 就不可能为 0),因此新线程就只能乖乖去等待队列中排队,无法竞争。
结尾
JDK8 源码解读:ReentrantLock - 变量与结构
源码
JDK 1.8 源码阅读-注释版
JDK 1.8 源码阅读
还没有评论,来说两句吧...