概述
ReentrantLock 是一个可重入锁,指的是一个线程能够对临界资源重复加锁,它是基于 AQS 的独占模式实现的同步组件,用同步状态 state
记录某个线程获取独占锁的次数,初始值为 0 ,获取锁时+1,释放锁时-1,并且它支持公平和非公平模式。
特性
说起 ReentrantLock ,就不得不提 synchronized 关键字,下面给出两者的对比关系:
特性 | ReentrantLock | synchronized |
---|---|---|
灵活性 | 支持响应中断、超时,尝试获取锁 | 不灵活 |
锁类型 | 公平锁或非公平锁 | 非公平锁 |
条件队列 | 可关联多个条件队列 | 关联一个条件队列 |
锁实现机制 | 依赖 AQS | JVM 监视器模式 |
可重入性 | 可重入 | 可重入 |
释放锁 | 必须显示调用 unlock() 释放锁 | 自动释放 |
源码分析
可重入锁 ReentrantLock
是基于 AQS 实现的独占模式的同步组件,既然是基于 AQS 实现,必然遵循使用 AQS 的固定步骤,在分析源码的过程会有所体现。由于可重入锁支持公平和非公平模式,并且实现方式有所区别,因此在分析源码过程中会按照这两种情况分别说明。下面我们就通过读源码的方式来了解 ReentrantLock
是如何通过公平锁和非公平锁与 AQS 关联起来的。
ReentrantLock 的类继承关系类图如下:
源码结构
1 | public class ReentrantLock implements Lock, java.io.Serializable { |
ReentrantLock 基于 AQS 实现的独占模式的可重入锁步骤如下:
- 定义继承自 AQS 的静态内部类 Sync
- 由于 ReentrantLock 实现的是独占模式的锁功能,因此需要重写
tryAcquire
和tryRelease
方法对- 将 Sync 的实现组合在 ReentrantLock 的加锁(如 lock() 方法)和释放锁(如 unlock() 方法)的实现中
由于 ReentrantLock
支持公平和非公平锁,获取同步状态方法 tryAcquire
在两种实现中有所差异,因此该方法交给各自实现。静态内部类 Sync
实现了公平和非公平锁统一使用的释放同步状态的方法 tryRelease
。
了解了 ReentrantLock
实现独占模式的可重入锁的套路后,下面我们继续对支持锁功能的静态内部类 Sync
进行分析。
Sync
1 | +--- ReentrantLock |
继承自 AQS 的静态内部类 Sync 主要有 5 类方法:
- lock 抽象方法,供子类实现,完成获取锁的逻辑
- nonfairTryAcquire 方法实现了非公平获取同步状态逻辑。没有定义在非公平锁实现类中是因为 ReentrantLock 中的其它方法也要使用,如 tryLock 方法
- tryRelease 方法实现了释放同步状态的逻辑,释放同步状态逻辑不区分是否公平
- isHeldExclusively 方法用于判断当前线程是否正在占有独占锁
- newCondition 方法用于创建当前 Lock 的 Condition 对象
非公平锁
1 | +--- ReentrantLock |
非公平锁实现类 NonfairSync
继承了 Sync
类,实现了 Sync
中定义获取锁的 lock
方法(非公平获取锁),逻辑体现在 lock
方法和 tryAcquire
方法中;实现了AQS中定义的模版方法 tryAcquire
,尝试以非公平方式获取独占模式的同步状态。
加锁
lock 方法用于非公平获取锁,逻辑如下:
- 若通过 CAS 设置同步状态 state 成功则获取锁成功,然后将当前线程设置为独占线程即可。
- 若通过 CAS 设置同步状态 state 失败则获取锁失败,然后进入
acquire
方法进行后续处理(先通过调用实现的tryAcquire
方法尝试再次获取锁,失败后则进入同步队列中)
加锁完整流程图
下面对加锁过程进行说明:
1 使用方通过 ReentrantLock 暴露出去的加锁方法 lock() 进行加锁操作。
2 将加锁操作交给内部实现 NonfairSync 的加锁方法 lock() 完成。如果加锁成功则获取锁过程结束,否则执行 AQS 的 acquire 方法。
3 AQS 的 acquire 方法会执行 tryAcquire 方法,该方法由 NonfairSync 实现用来非公平尝试获取同步状态。
4 tryAcquire 是获取锁逻辑,获取失败后,会执行 AQS 框架的后续逻辑,跟 ReentrantLock 自定义同步组件无关。
关于 AQS 框架的逻辑就不再展开说明,感兴趣地可以参考 AQS原理分析 。
公平锁
1 | /** |
公平锁实现类 FairSync
继承了 Sync
类,实现了 Sync
中定义获取锁的 lock
方法(公平获取锁),逻辑体现在 tryAcquire
方法中;实现了AQS中定义的模版方法 tryAcquire
,尝试以公平方式获取独占模式的同步状态。
加锁
lock 方法用于公平获取锁,是直接调用 AQS 模版方法 acquire 完成的,其获取锁的逻辑封装在实现的 tryAcquired
方法中。
加锁完整流程图
下面对加锁过程进行说明:
1 使用方通过 ReentrantLock 暴露出去的加锁方法 lock() 进行加锁操作。
2 将加锁操作交给内部实现 FairSync 的加锁方法 lock() 完成,lock() 方法直接执行 AQS 的 acquire 方法。
3 AQS 的 acquire 方法会执行 tryAcquire 方法,该方法由 FairSync 实现用来公平尝试获取同步状态。
4 tryAcquire 是获取锁逻辑,获取失败后,会执行 AQS 框架的后续逻辑,跟 ReentrantLock 自定义同步组件无关。
公平锁的加锁逻辑几乎和非公平锁的加锁逻辑一致,唯一不同的地方在于,在获取锁时即使没有线程持有锁,对公平锁来说要讲究先来后到,此时可能在同步队列中有等待的线程节点,此时不能抢占获取锁。
释放锁
对 ReentrantLock
来说,释放锁是不区分公平性的,因此释放锁的逻辑封装在了父类 Sync 中,实现如下:
1 | +--- ReentrantLock |
下面对 ReentrantLock 释放锁的操作进行简单概括:
- 使用方通过调用 ReentrantLock 的解锁方法 unlock() 进行解锁。
- unlock() 方法会调用内部类 Sync 的 release 模版方法,该方法是 AQS 的方法。
- release 中会调用 tryRelease 方法,tryRelease 需要自定义同步组件自行实现,该方法只在父类 Sync 中实现,因此可以看出,释放锁的过程并不区分是否公平。
- 释放锁成功后,所有处理由 AQS 框架完成,与自定义同步组件无关。
至此,可重入锁 ReentrantLock 的加锁和释放锁整个过程已经分析完毕。下面对整个流程进行简单总结,具体如下图所示:
其它
1 | +--- ReentrantLock |
ReentrantLock 除了我们就常用的加锁方法 lock()
和解锁方法 unlock
外,还提供了上述的系列方法。值得一说的是 newCondition
方法,该方法是使用 ReentrantLock
和 Condition
实现等待-通知机制的,具体的实现原理可参考 AQS 原理分析 - Condition实现原理 。
差异
公平锁和非公平锁的差异如下:
- 非公平锁在调用 lock 后,首先会使用 CAS 尝试抢占锁,如果此时锁没有被占用,那么获取锁成功并返回。
- 非公平锁 CAS 尝试抢占锁失败后,和公平锁一样都会进入自己实现的
tryAcquire
方法,在该方法中如果发现同步状态 state == 0 ,非公平锁会直接 CAS 抢占锁,而公平锁会判断同步队列中是否有线程节点处于等待状态,如果有则不去抢占锁,进入排队流程。
相对来说,非公平锁会有更好的性能,因为它的吞吐量更大。但非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
小结
ReentrantLock 基于 AQS 实现了锁的机制,支持公平和非公平模式。核心思想是使用同步状态 state 控制 ReentrantLock 可重入的情况,state 初始化的时候为0,表示没有任何线程持有锁。当有线程持有该锁时,值就会在原来的基础上+1,同一个线程允许多次获得锁,此时就会多次+1,这就是可重入的概念。解锁也是对这个字段-1,一直到0,此线程释放锁。拿不到锁就借助 AQS 的同步队列管理等待线程。