目录
- 1.ReentrantLock 是什么?
- 2.✨什么是重入锁?ReentrantLock 是如何实现可重入特征的?
- 3.公平锁和非公平锁有什么区别?ReentrantLock 分别是如何实现的?
- 4.✨ReentrantLock 的实现原理是什么?
- 5.为什么 ReentrantLock 默认使用非公平锁?
- 6.✨synchronized 和 ReentrantLock 有什么区别?
- 7.将 ReentrantLock 放在方法内部声明会有什么问题?为什么不能放在方法里面?
- 8.可中断锁和不可中断锁有什么区别?
在了解 ReentrantLock 之前可以先查看这篇文章 Java 并发编程面试题——Lock 与 AbstractQueuedSynchronizer (AQS)
1.ReentrantLock 是什么?
(1)ReentrantLock 实现了 Lock
接口,是一个可重入且独占式的锁,和 synchronized 关键字类似。不过,ReentrantLock 更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。
public class ReentrantLock implements Lock, Serializable {
//...
}
(2)ReentrantLock 里面有一个内部类 Sync
,Sync 继承 AQS (AbstractQueuedSynchronizer)
,添加锁和释放锁的大部分操作实际上都是在 Sync 中实现的。Sync 有公平锁 FairSync
和非公平锁 NonfairSync
两个子类。
(3)ReentrantLock 默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。
// fair 为 true 时为公平锁,false 时为非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁。如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。
2.✨什么是重入锁?ReentrantLock 是如何实现可重入特征的?
(1)重入锁是指支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁,ReentrantLock 本身就是重入锁。重入锁具备重入特性,即允许同一个线程多次获取锁,而不会导致线程死锁。这使得编程更加灵活,可避免一些潜在的问题。
(2)可重入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特性的实现需要解决以下两个问题:
- 线程再次获取锁:锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
- 锁的最终释放:线程重复 n 次获取了锁,随后在第 n 次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于 0 时表示锁已经成功释放。
(3)ReentrantLock 是通过组合自定义同步器来实现锁的获取与释放,以非公平性(默认的)实现为例,其处理逻辑如下:
- 线程再次获取锁:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值 state 进行加一操作并返回
true
,表示获取同步状态成功; - 锁的最终释放:成功获取锁的线程再次获取锁,只是增加了同步状态值,这也就要求
ReentrantLock
在释放同步状态时减少同步状态值。如果该锁被获取了 n 次,那么前 (n - 1) 次tryRelease(int releases)
方法必须返回 false,而只有同步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为 0 作为最终释放的条件,当同步状态为 0 时,将占有线程设置为 null,并返回 true,表示释放成功;
思考:为什么需要可重入锁?
回答:在多线程编程中,可能会出现线程需要在多个方法中嵌套使用同一把锁的情况。如果没有可重入锁的支持,那么一个线程在持有锁的情况下再次请求这个锁时,就会发生自己阻塞的情况,导致出现死锁。可重入锁的出现则解决了这个问题,它允许线程多次获取同一把锁,确保在未释放锁之前,线程仍然可以正常地访问该锁所保护的资源。
3.公平锁和非公平锁有什么区别?ReentrantLock 分别是如何实现的?
(1)公平锁和非公平锁之间的区别如下:
- 公平锁 : 锁被释放之后,会严格按照请求线程的顺序来分配锁,即先到先得。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。
- 非公平锁:锁被释放之后,允许线程通过插队来抢占锁资源,即后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。
(2)ReentrantLock 默认使用非公平锁,其内部使用了 AQS 来实现锁资源的竞争,没有竞争到锁资源的线程会加入到 AQS 的同步队列中,该同步队列是由一个双向链表实现的:
- ReentrantLock 实现公平锁的方式是线程在竞争锁资源时会判断同步队列中有没有等待线程,如果有就加入到同步队列的尾部进行等待;
- 而非公平锁的实现方式是不管同步队列里面有没有线程等待,它都会先去尝试抢占锁资源,如果抢占不到,才会加入到同步队列的尾部进行等待。
4.✨ReentrantLock 的实现原理是什么?
ReentrantLock 实现了 Lock
接口,是一个可重入且独占式的锁,ReentrantLock 的底层实现有以下几个非常重要的方面:
- 锁的竞争:
- ReentrantLock 有一个非常重要的内部类
Sync
,Sync 继承AQS
,添加锁和释放锁的大部分操作实际上都是在 Sync 中实现的。 - AQS 采用了一个 int 类型的互斥变量
state
用来记录锁竞争的一个状态,0 表示当前没有任何线程竞争锁资源,而大于等于 1 表示已经有线程正在持有锁资源。一个线程来获取锁资源的时候,首先判断 state 是否等于 0,如果是(无锁状态),则把这个 state 更新成 1,表示占用到锁。此时如果多个线程进行同样的操作,会造成线程安全问题。AQS 采用了 CAS 机制来保证state
的原子性。 - 对于没有竞争到锁的线程,它们会被存储到 AQS 的同步队列中,该同步队列的底层是通过双向链表来实现的。当锁被释放之后,会从 AQS 队列里面的头部唤醒下一个等待锁的线程。具体过程如下图所示:
- ReentrantLock 有一个非常重要的内部类
- 锁的可重入:可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁,其余具体实现过程见上面的分析;
- 锁的公平性和非公平性:Sync 有公平锁
FairSync
和非公平锁NonfairSync
两个子类,其余具体实现过程见上面的分析;
5.为什么 ReentrantLock 默认使用非公平锁?
(1)ReentrantLock 默认使用非公平锁是为了优化性能。非公平锁在获取锁时不会考虑等待队列中其他线程的顺序,而是允许新到来的线程插队获取锁,这在某些情况下可以减少线程切换的开销,提高整体的吞吐量。
(2)而公平锁则会遵循先进先出的原则,保证所有线程按照请求的顺序获取锁,这会增加线程切换的开销,但可以避免饥饿的发生。由于大多数情况下公平性不是一个强制要求,因此非公平锁被认为是默认的更合理选择。不过,如果有特定的需求,ReentrantLock 也提供了可以配置的公平锁选项。
6.✨synchronized 和 ReentrantLock 有什么区别?
(1)synchronized 和 ReentrantLock 都是用于实现线程同步的机制,它们的主要区别在于以下几个方面:
- 本质:synchronized 是⼀个关键字,ReentrantLock 是⼀个类;
- 加锁方式:synchronized 会自动的加锁与释放锁,ReentrantLock 则需要通过
lock()
方法与unlock()
方法手动加锁与释放锁; - 公平性:ReentrantLock 默认情况是非公平的,但可以通过 ReentrantLock 类的
ReentrantLock(boolean fair)
构造方法来制定是否是公平的;而 synchronized 只能是非公平锁,即无法保证等待时间最长的线程先获得锁。 - 锁性质:synchronized 是 JVM 层面的锁;ReentrantLock 是 API 层面的锁;
- 中断支持:ReentrantLock 提供了可以响应中断的获取锁的方式,而 synchronized 没有中断的语义。
- 等待可中断:ReentrantLock 提供了一个
tryLock(long time, TimeUnit unit)
方法,该方法可以尝试获取锁一段时间,在指定时间内获取不到锁会返回 false,从而避免了线程一直阻塞的情况。 - 锁状态保存:synchronized 锁的是对象,锁信息保存在对象头中;ReentrantLock 通过代码中 int 类型的
state
变量来标识锁的状态; - 条件变量:ReentrantLock 可以配合
Condition
接口实现更灵活的线程通信和同步。 - 性能:在高并发情况下,ReentrantLock 通常比 synchronized 性能更好。
(2)综上所述,虽然 synchronized 简单易用,但是在一些复杂的场景下可能无法满足需求,此时可以使用 ReentrantLock 来实现更灵活的线程同步。但需要注意的是,由于 ReentrantLock 比 synchronized 更加复杂,使用不当可能会导致死锁等问题,因此在使用 ReentrantLock 时需要格外小心。
相关知识点:
Java 并发编程面试题——synchronized 与 volatile
Java 并发编程面试题——Condition 接口
7.将 ReentrantLock 放在方法内部声明会有什么问题?为什么不能放在方法里面?
(1)ReentrantLock
是 Java 提供的一个可重入锁实现,用于在多线程环境中提供更灵活的同步控制。通常情况下,应该将 ReentrantLock
的声明放在类的成员变量位置,而不是放在方法内部。如果将 ReentrantLock
放在方法内部声明,会导致以下问题:
- 互斥性失效:每次方法调用都会创建一个新的
ReentrantLock
对象,这意味着每个线程都会获取到一个独立的锁。因此,无法实现多个线程之间的互斥访问,即使是同一个线程多次调用同一个方法,也无法保证其互斥性。 - 数据共享困难:将
ReentrantLock
放在方法内部,会导致每个线程拥有自己的锁对象。这会造成在线程间共享数据时的困扰,因为多个线程无法共享同一个锁对象来保护共享数据的访问。如果不同的线程持有不同的锁对象,那么锁的语义将失效,无法保证数据的一致性和正确性。 - 锁无法释放:当将
ReentrantLock
放在方法内部声明时,每个线程都会创建一个独立的锁对象,并在其方法执行完毕后释放锁。这意味着即使其他线程仍然需要访问该方法所保护的共享资源,它们无法获取到被锁保护的权限,从而造成了资源的长时间阻塞和不必要的等待。
(2)综上所述,不推荐将 ReentrantLock
放在方法内部声明。相反,应该将 ReentrantLock
的声明放在类的成员变量位置,使得所有线程都共享同一个锁对象,并确保互斥性、数据一致性和正确的资源释放。
8.可中断锁和不可中断锁有什么区别?
- 可中断锁 :获取锁的过程中可以被中断,不需要一直等到获取锁之后才能进行其他逻辑处理。ReentrantLock 就属于是可中断锁。
- 不可中断锁 :一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理。 synchronized 就属于是不可中断的。