前言
本文主要讲解底层逻辑,基本不会贴代码,目的是让大家能够真正的知晓原理,对照着逻辑去理解代码看代码也会很快就能看懂。
在讲ReentrantLock原理之前,我们先回顾下ReentrantLock的基本用法。ReentrantLock是一个锁编程api,让我们达到类似syncronized类似的效果,但它可以提供更多的功能,例如公平锁等。通常的写法如下所示:
class X {
private final ReentrantLock lock = new ReentrantLock();
public void m() {
doSomething();
// 需要加锁的代码块
lock.lock(); // 加锁
try {
// 业务处理
} finally {
lock.unlock() // 释放锁,一定要放在finalyy块中
}
}
}
ReentrantLock的重点
公平和非公平区别在哪里?
理解ReentrantLock一切的起点从暴露出来的接口开始。接下来,我们探索的入口就是从这个lock()方法开始:
内部调用的是sync.lock(),我们经常讲的ReentrantLock有公平锁和非公平锁两种实现,这里的sync就是非公平同步器和公平同步器之一。为了高性能,ReentrantLock 默认采用了非公平锁实现方式。相关类的继承关系如下所示:
RentrantLock实际上是一层包装,根据公平和非公平需要,持有相应的公平或者非公平的同步器。内部基本所有的功能都是通过Sync及其子类实现的,Sync的lock方法是一个抽象方法,实现是通过子类实现的,公平锁和非公平锁lock操作的流程如下所示:
非公平锁加锁流程会先有一个cas的操作尝试获取锁,成功了就执行业务代码块,不用再去排队。如果cas失败,就走正常的尝试获取锁、入队流程。因为少了入队而产生的阻塞、唤醒,使得非公平锁的性能很好。这样也会带来一个问题,就是如果新来的线程总是能抢到锁的话,队列里的线程就因为老是抢不到锁而处于饥饿状态。
公平锁加锁流程就比较简单了,直接进行尝试获取锁、入队流程。这里的尝试获取锁里面有特殊判断(接下来马上会讲),不会造成非公平的现象,可以理解不管谁来都老老实实的去排队。
公平锁和非公平锁在尝试获取锁的操作上面会有一丝不同。如果是非公平锁,会直接通过cas再次尝试加锁(够无赖的)。而公平锁会首先判断队列里面是否已经有等待的线程了,如果有则老老实实去排队,如果没有,那就各凭本事去抢锁了,很公平。两个tryAcquire的流程如下所示:
小结:非公平锁加锁流程会先有一个cas的操作尝试获取锁,成功了就执行业务代码块,不用再去排队。如果cas失败,就走正常的尝试获取锁、入队流程。公平锁加锁会判断是否有线程在队列中,如果没有则直接进行尝试获取锁,成功了就执行业务代码块,失败则开始入队。如果有线程已经在队列中,则进行入队。
可重入的实现方式
可重入性就是一个线程不用释放,可以重复的获取一个锁n次,只是在释放的时候,也需要相应的释放n次。可重入锁的实现解决了同一个线程需要多次获取同一个锁的问题。
要实现可重入锁,至少需要保存两个东西:现在持有锁的线程和已经上锁的次数。刚才讲上锁流程的时候,我们已经提到过两个对象exclusiveOwnerThread和state。exclusiveOwnerThread保存了持有锁的线程,state在>0的情况下保存了线程对锁的重入次数,这样在释放的时候减去相应的次数即可,等state=0时就说明已经完全释放掉了,这时候再将exclusiveOwnerThread设置为null。
到此为止属于ReentrantLock的内容实际上基本就结束了,剩下的部分全部是AQS实现的,所以我们的重点要转向去研究AQS的结构和流程。
AQS
AQS的结构
AQS的结构不难,我们要做到的就是把下面这个图能自己闭着眼睛画出来。
我们上面讲的获取锁,释放锁,操作的就是这个state对象。state的含义并非固定的,在ReentrantLock中,state初始化的时候是0,表示没有上锁,=1表示有线程对它加了锁,>1表示重入了多少次。在CountDownLatch中,它表示有多少个任务需要等待完成。ReentrantLock中state状态流转如下所示:
AQS的所有结点都是Node对象,Node对象结构在上面的类图里面已经画出来了,我们需要初步的理解它:
pre/next:没什么好说的,指向前驱、后驱的结点指针。
thread:保存了入队列的线程。
waitStatus:重点理解对象!!waitStatus包括CANCELLED(线程已经被删除了)、SIGNAL(后面的线程需要被唤醒)、CONDITION(线程在等待condition条件)、PROPAGATE(共享锁需要传递),先了解即可。
nextWaiter:指向下个等待条件的Node对象,如果不是Condition的对象,默认是Node.EXCLUSIVE,表示这是个排他锁。
在有线程获取锁失败入队的情况下,整个AQS的结构大概就是这个样子:
入队流程
以非公平锁为例,尝试加锁失败后会进入到AQS队列中。队列的对象都是Node对象,所以加入队列前会根据这个线程创建一个Node对象,Node结点的nextWaiter设置成了Node.EXCLUSIVE,然后将该结点入队,流程如下所示:
对应代码为private Node addWaiter(Node mode)。
队列中的线程怎么抢锁
线程入队了,那么队列中的线程怎么抢锁呢?看起来似乎很简单,既然都在队列中了,那肯定就是队列头的线程先拿到锁呗。但是我们需要考虑几个问题:
1、队列中的线程抢锁什么时候触发?
2、我们知道线程没有抢到锁会被阻塞,轮到它时,该怎么被唤醒?
3、队列中如果有无效的线程怎么办?
我们带着这几个问题去看看队列的处理。
实际上,在线程抢锁失败后包装成一个Node而入队,在入队完成后,紧接着就会触发队列抢锁操作,如下所示:
现在我们知道队列的中的线程抢锁,实际上是从入队的这个线程触发的,整个处理的流程被包在了一个死循环里面,具体的流程如下所示:
在初始化队列的时候,我们设置的了head,tail,如果入队的这个线程是第一个线程,那么它的前驱结点刚好就是head,说明它在队列头,该它去抢锁了。如果不是,理论上该线程就应马上被阻塞,但是阻塞前会做一些额外判断工作,这个时候我们的waitStatus就排上用场了!
waitStatus
waitStatus包括CANCELLED(1:线程已经被删除了)、SIGNAL(-1:后面的线程需要被唤醒)、CONDITION(-2:线程在等待condition条件)、PROPAGATE(-3:共享锁需要传递),状态流转如下所示:
Condition和Propogate两种状态分别用于lock.newConditon和共享锁,这里暂不涉及。阻塞前的waitStatus判断和处理如下所示:
当需要被阻塞的线程设置好了“哨兵”后,就放心的调用LockSupport.park方法阻塞自己。那么B什么时候会被唤醒呢?当A线程业务处理完成后,调用unlock方法释放琐,里面就会唤醒后续的线程,流程如下所示:
释放锁跟加锁差不多的操作,重点就在这个唤醒上面,唤醒的流程如下所示:
因为头结点是一个dummy结点,现在要唤醒它后面的线程,waitStatus需要重新恢复成0的状态。然后拿到后继线程结点,这个拿很有讲究,首先是通过h.next尝试获取,然后看看是否能拿到。如果能拿到而且是正常阻塞的线程,那万事大吉直接唤醒它。如果不幸是空或者已经删除了,则从tail往前找啊找,找到最前面的一个正常结点(没有破坏FIFO的性质,waitStatus < 0为正常状态,这里情况就是SIGNAL。队列中如果有无效的线程就会被跳过),然后唤醒它。关于为什么采用tail往前遍历的做法,可以参考这个:https://www.zhihu.com/question/50724462?sort=created。
唤醒线程后,线程就继续在死循环里去抢锁了。
对应的代码在final boolean acquireQueued(final Node node, int arg)中。
condition怎么玩的?
condition用于线程同步编程,实际上它拥有自己的队列,限于篇幅这个放在另外的文章里。
总结
ReentrantLock是最常用的锁API,在线程池ThreadPoolExecutor、阻塞队列LinkedBlockingQueue,同步工具CyclicBarrier等中都能看到它的身影。掌握好ReentrantLock的原理,可以让我们更加清楚基于它的实现的其他工具类的实现原理。
ReentrantLock实现了类似Syncronized的同步能力,但它还支持了公平锁的能力。根据公平和非公平需要,ReentrantLock持有相应的公平或者非公平的同步器。非公平锁加锁流程会先有一个cas的操作尝试获取锁,成功了就执行业务代码块,不用再去排队。如果cas失败,就走正常的尝试获取锁、入队流程。公平锁尝试加锁会判断是否有线程在队列中,如果没有则直接进行尝试获取锁,成功了就执行业务代码块,失败则开始入队。如果有线程已经在队列中,则进行入队操作。NonfairSync和FairSync的区别体现在lock和tryAcquire两个方法上面。
ReentrantLock通过exclusiveOwnerThread和state来实现可重入,exclusiveOwnerThread保存了持有锁的线程,state在>0的情况下保存了线程对锁的重入次数,这样在释放的时候减去相应的次数即可,等state=0时就说明已经完全释放掉了,这时候exclusiveOwnerThread设置为null。
AQS需要对队列和Node的类结构铭记于心,尤其是state和waitStatus含义,理解了它俩,上锁和释放锁流程就会变得非常容易。