引言
ReentrantLock是面试中的高频考点,其中实现原理还是很有必要了解的。它与synchronized类似,都是互斥锁,但具有更好的扩展性。ReentrantLock是基于AQS实现的,遗忘的同学可以回顾一下AQS源码详细解读。
文章导读
- ReentrantLock继承树及重要方法
- 非公平锁及公平锁的获取
- tryLock(),lockInterruptibly()
- 释放资源
- ReentrantLock相关面试题
- 总结
一、ReentrantLock的内部类概述与重要方法
1.1 继承关系概述
首先看一下继承关系图,对它整体的构造有一个初步的认识。
我们发现,ReentrantLock实现了公平锁和非公平锁。都通过他们的父类Sync来调度。
- Sync:是提供AQS实现的工具,类似于适配器,提供了抽象的lock(),便于快速创建非公平锁。
- FairSync(公平锁):线程获取锁的顺序和调用lock()的顺序一样,FIFO。
- NoFairSync(非公平锁):线程获取锁的顺序和调用lock()的顺序无关,抢到CPU的时间片即可调度。
1.2 ReentrantLock中的重要方法
构造方法:无参构造方法,默认创建非公平锁;有参构造方法,并且fair==true时,创建公平锁。
//维护了一个Sync,对于锁的操作都交给sync来处理
获取资源资源(锁)的方法:可以看出,请求都是交给Sync来调度的。
//请求锁资源,会阻塞且不处理中断请求,
释放资源(锁)的方法:不管是公平还是非公平锁,都会调用AQS.release(1),给当前线程持有锁的数量-1。
public
二、获取资源(锁)
我们主要看非公平锁与公平锁获取资源的方法,因为释放资源的逻辑是一样的。
2.1 Sync获取资源
sync中定义了获取资源的总入口。具体的调用还是看实现类是什么。
abstract
2.2 非公平锁获取资源
lock():获取锁时调用AQS的CAS方法,是阻塞的。如果获取成功,则把当前线程设置为锁的持有者;如果获取失败,则通过AQS.acquire()获取锁。通过AQS源码详细解读,了解到acquire()中使用了模板模式,调用子类的tryAcquire()尝试获取锁,如果tryAcquire()返回false,则进入等待队列自旋获取,再判断前驱的waitStatus,判断是否需要被阻塞等。这里就不一一赘述了,感兴趣的可以看上一篇文章。
final
tryAcquire():走的是Sync.nofairTryAcquire()。
protected
nonfairTryAcquire(int acquires):如果锁空闲,则用CAS修改state;如果锁被占用,则判断占有者是不是自己,实现可重入。最终没有获取锁到就返回false。
final
2.3 公平锁获取资源
lock():也是阻塞的。与非公平锁的区别是,不能直接通过CAS修改state,而是直接走AQS.acquire()。
final
tryAquire():与非公平锁类似,AQS.acquire()会调用这个钩子方法。只不过多判断了hasQueuedPredecessors(),判断当前节点在等待队列中是否有前驱节点,如果有,则说明有线程比当前线程更早的请求资源,根据公平性,当前线程请求资源失败;如果当前节点没有前驱节点,才有做后面的逻辑判断的必要性。
protected
三、其他获取锁的方法
ReentrantLock有3中获取锁的方法,lock(),tryLock(),lockInterruptibly()。
3.1 tryLock()--尝试获取资源
tryLock():走的还是sync的方法,在指定时间内获取锁,直接返回结果。
public
tryAcquireNanos():如果调用tryLock的规定时间内尝试方法,就会调用该方法,先判断是否中断,然后尝试获取资源,否则进入AQS.doAcquireNanos()(这个方法在上篇文章有解释)。在规定时间内自旋拿资源,拿不到则挂起再判断是否被中断。
public
3.2 lockInterruptibly()--获取锁时响应中断
lockInterruptibly():交给了调度者sync执行。
public
acquireInterruptibly():当尝试获取锁失败后,就进行阻塞可中断的获取锁的过程。调用AQS.doAcquireInterruptibly()(这个方法在上篇文章也有详细解释)。
public
四、释放资源(锁)
公平锁与非公平锁的释放都是一样的。通过前面的阅读,可以知道,ReentrantLock.release()调用的是sync.release(1)。本质还是进入AQS.release(1),下面看看其中的tryRelease()这个钩子方法如何实现。
Sync释放资源
tryRelease():尝试释放锁,彻底释放后返回true。
protected
五、ReentrantLock的相关面试题
1)ReentrantLock是如何实现可重入的?
不管是公平锁还是非公平锁,在获取锁时调用的tryAcquire()方法,获取成功后会setExclusiveOwnerThread(current)。将本线程设置为主人,之后每次调用tryAcquire()时,发现当前线程就是主人,直接返回true。
2)简述公平锁与非公平锁的区别?
从定义角度:
获取锁的顺序与请求锁的时间顺序一致就是公平锁,反之则为非公平锁。公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁。
公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量。
从源码角度:
当锁资源已经被占用时,请求每次有请求到达,就在等待队列中排队。此时如果锁资源被释放了,刚好新来一个线程,若是非公平锁则会直接CAS获取锁,成功则返回,不成功则加入到等待队列自旋获取,自旋过程中当前驱是对头,并且tryAcquire成功时则获取成功。
若是公平锁,则当前线程必须等待,锁必须给等待队列第一个线程,如果第一个线程被阻塞了,唤醒也是需要时间的,醒了才能拿锁。
3)AQS中有哪些资源访问模式?区别?
独占模式和共享模式。
只有一个线程才能持有这个锁就是独占模式,由Node节点中的nextWait来标识。
ReentrantLock就是一个独占锁;而WriteAndReadLock的读锁则能由多个线程同时获取,但它的写锁则只能由一个线程持有,因此它使用了两种模式。
4)为什么ReentrantLock.lock()方法不能被其他线程中断?
因为当前线程前面可能还有等待线程,在AQS.acquireQueued()的循环里,线程会再次被阻塞。parkAndCheckInterrupt()返回的是Thread.interrupted(),不仅返回中断状态,还会清除中断状态,保证阻塞线程忽略中断。
总结
其实看完AQS源码后,ReentrantLock就是个弟弟。在实现上其实并不复杂,实现了AQS的独占模式。希望看完之后能对可重入锁,响应中断锁有更深入的理解。
如果喜欢我的文章,欢迎关注我的专栏~