问题
(1)重入锁是什么?
(2)ReentrantLock如何实现重入锁?
(3)ReentrantLock为什么默认是非公平模式?
(4)ReentrantLock除了可重入还有哪些特性?
简介
Reentrant = Re + entrant,Re是重复、又、再的意思,entrant是enter的名词或者形容词形式,翻译为进入者或者可进入的,所以Reentrant翻译为可重复进入的、可再次进入的,因此ReentrantLock翻译为重入锁或者再入锁。
重入锁,是指一个线程获取锁之后再尝试获取锁时会自动获取锁。
在Java中,除了ReentrantLock以外,synchronized也是重入锁。
那么,ReentrantLock的可重入性是怎么实现的呢?
继承体系

ReentrantLock实现了Lock接口,Lock接口里面定义了java中锁应该实现的几个方法:
// 获取锁
Lock接口中主要定义了 获取锁、尝试获取锁、释放锁、条件锁等几个方法。
源码分析
主要内部类
ReentrantLock中主要定义了三个内部类:Sync、NonfairSync、FairSync。
abstract
(1)抽象类Sync实现了AQS的部分方法;
(2)NonfairSync实现了Sync,主要用于非公平锁的获取;
(3)FairSync实现了Sync,主要用于公平锁的获取。
在这里我们先不急着看每个类具体的代码,等下面学习具体的功能点的时候再把所有方法串起来。
主要属性
private
主要属性就一个sync,它在构造方法中初始化,决定使用公平锁还是非公平锁的方式获取锁。
主要构造方法
// 默认构造方法
(1)默认构造方法使用的是非公平锁;
(2)第二个构造方法可以自己决定使用公平锁还是非公平锁;
上面我们分析了ReentrantLock的主要结构,下面我们跟着几个主要方法来看源码。
lock()方法
彤哥贴心地在每个方法的注释都加上方法的来源。
公平锁
这里我们假设ReentrantLock的实例是通过以下方式获得的:
ReentrantLock
下面的是加锁的主要逻辑:
// ReentrantLock.lock()
看过之前彤哥写的【死磕 java同步系列之自己动手写一个锁Lock】的同学看今天这个加锁过程应该思路会比较清晰。
下面我们看一下主要方法的调用关系,可以跟着我的 → 层级在脑海中大概过一遍每个方法的主要代码:
ReentrantLock
获取锁的主要过程大致如下:
(1)尝试获取锁,如果获取到了就直接返回了;
(2)尝试获取锁失败,再调用addWaiter()构建新节点并把新节点入队;
(3)然后调用acquireQueued()再次尝试获取锁,如果成功了,直接返回;
(4)如果再次失败,再调用shouldParkAfterFailedAcquire()将节点的等待状态置为等待唤醒(SIGNAL);
(5)调用parkAndCheckInterrupt()阻塞当前线程;
(6)如果被唤醒了,会继续在acquireQueued()的for()循环再次尝试获取锁,如果成功了就返回;
(7)如果不成功,再次阻塞,重复(3)(4)(5)直到成功获取到锁。
以上就是整个公平锁获取锁的过程,下面我们看看非公平锁是怎么获取锁的。
非公平锁
// ReentrantLock.lock()
相对于公平锁,非公平锁加锁的过程主要有两点不同:
(1)一开始就尝试CAS更新状态变量state的值,如果成功了就获取到锁了;
(2)在tryAcquire()的时候没有检查是否前面有排队的线程,直接上去获取锁才不管别人有没有排队呢;
总的来说,相对于公平锁,非公平锁在一开始就多了两次直接尝试获取锁的过程。
lockInterruptibly()方法
支持线程中断,它与lock()方法的主要区别在于lockInterruptibly()获取锁的时候如果线程中断了,会抛出一个异常,而lock()不会管线程是否中断都会一直尝试获取锁,获取锁之后把自己标记为已中断,继续执行自己的逻辑,后面也会正常释放锁。
题外话:
线程中断,只是在线程上打一个中断标志,并不会对运行中的线程有什么影响,具体需要根据这个中断标志干些什么,用户自己去决定。
比如,如果用户在调用lock()获取锁后,发现线程中断了,就直接返回了,而导致没有释放锁,这也是允许的,但是会导致这个锁一直得不到释放,就出现了死锁。
lock
当然,这里只是举个例子,实际使用肯定是要把lock.lock()后面的代码都放在try...finally...里面的以保证锁始终会释放,这里主要是为了说明线程中断只是一个标志,至于要做什么完全由用户自己决定。
tryLock()方法
尝试获取一次锁,成功了就返回true,没成功就返回false,不会继续尝试。
// ReentrantLock.tryLock()
tryLock()方法比较简单,直接以非公平的模式去尝试获取一次锁,获取到了或者锁本来就是当前线程占有着就返回true,否则返回false。
tryLock(long time, TimeUnit unit)方法
尝试获取锁,并等待一段时间,如果在这段时间内都没有获取到锁,就返回false。
// ReentrantLock.tryLock()
tryLock(long time, TimeUnit unit)方法在阻塞的时候加上阻塞时间,并且会随时检查是否到期,只要到期了没获取到锁就返回false。
unlock()方法
释放锁。
// java.util.concurrent.locks.ReentrantLock.unlock()
释放锁的过程大致为:
(1)将state的值减1;
(2)如果state减到了0,说明已经完全释放锁了,唤醒下一个等待着的节点;
未完待续,下一章我们继续学习ReentrantLock中关于条件锁的部分
彩蛋
为什么ReentrantLock默认采用的是非公平模式?
答:因为非公平模式效率比较高。
为什么非公平模式效率比较高?
答:因为非公平模式会在一开始就尝试两次获取锁,如果当时正好state的值为0,它就会成功获取到锁,少了排队导致的阻塞/唤醒过程,并且减少了线程频繁的切换带来的性能损耗。
非公平模式有什么弊端?
答:非公平模式有可能会导致一开始排队的线程一直获取不到锁,导致线程饿死。
推荐阅读
- 死磕 java同步系列之AQS起篇
- 死磕 java同步系列之自己动手写一个锁Lock
- 死磕 java魔法类之Unsafe解析
- 死磕 java同步系列之JMM(Java Memory Model)
- 死磕 java同步系列之volatile解析
- 死磕 java同步系列之synchronized解析
欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。
