问题
ReentrantLock是什么?为什么已经有synchronized了还需要它?ReentrantLock和synchronized有什么区别?ReentrantLock为什么叫“可重入锁”?可重入是怎么实现的?ReentrantLock底层是怎么实现的?- AQS 是什么
state表示什么exclusiveOwnerThread表示什么
- 一个线程调用
lock()以后,完整的加锁流程是什么?- CAS 抢锁
- 抢锁失败以后怎么办
- 什么时候进入等待队列
- 什么时候阻塞线程
- AQS 的等待队列是什么结构?
- CLH 队列
headtailNode
- 等待线程是怎么被阻塞和唤醒的?
LockSupport.park()LockSupport.unpark()
unlock()的完整流程是什么?state怎么减少- 什么时候真正释放锁
- 怎么唤醒下一个线程
ReentrantLock是怎么实现可重入的?- 公平锁和非公平锁有什么区别?
new ReentrantLock(true)- 为什么默认是非公平锁
- 非公平锁为什么吞吐量通常更高
tryLock()和lock()有什么区别?- 立即失败
- 超时获取
- 适合什么场景
lockInterruptibly()是什么?和lock()有什么区别?Condition是什么?为什么有了wait/notify还需要Condition?- 一个
ReentrantLock为什么可以创建多个Condition? Condition.await()和signal()的底层流程是什么?- Condition Queue
- AQS Queue
- await 为什么会释放锁
- signal 为什么不是直接让线程继续执行
ReentrantLock使用时为什么必须在finally里unlock()?ReentrantLock会发生死锁吗?常见死锁场景有哪些?ReentrantLock和synchronized在性能上谁更好?- 实际业务中什么时候更适合用
synchronized,什么时候更适合用ReentrantLock? - 如果让你手写一个简化版互斥锁,你会怎么基于 AQS 实现?
为什么已经有 synchronized 了,还需要 ReentrantLock?
ReentrantLock 本质上也是一种互斥锁,用来控制多个线程对共享资源的访问。
它和 synchronized 解决的是同一类问题,只不过 ReentrantLock 把锁做成了一个可以显式操作的 Java 对象。
例如:
|
|
相比 synchronized,ReentrantLock 提供了更强的控制能力。
synchronized 的加锁和释放基本由 JVM 自动完成,而 ReentrantLock 可以让开发者主动控制锁的获取过程,比如尝试获取锁、超时获取、响应中断,还可以选择公平锁,并且支持多个 Condition 等待队列。
所以有了 synchronized 之后仍然需要 ReentrantLock,主要不是因为它“更高级”或者一定“性能更好”,而是因为它提供了更多锁控制能力。
如果只是普通的互斥同步,synchronized 往往已经够用;如果业务需要 tryLock()、超时、可中断、公平锁或者更灵活的线程等待通知机制,就更适合使用 ReentrantLock。
ReentrantLock 提供了哪些能力
ReentrantLock 和 synchronized 一样,本质上都是互斥锁,但它提供了更多显式控制能力。
首先,它支持可重入。同一个线程已经拿到锁以后,可以再次获取同一把锁,不会把自己阻塞住。
其次,它支持尝试获取锁:
|
|
线程可以尝试拿锁,如果当前拿不到,可以直接返回,而不是一直等待。
还可以设置超时时间:
|
|
表示最多等待一段时间,超时以后就放弃。
ReentrantLock 还支持可中断地等待锁:
|
|
线程在等待锁的过程中,可以响应中断,而不是只能一直等到拿到锁。
除此之外,它还可以选择公平锁和非公平锁:
|
|
公平锁会尽量按照线程等待的先后顺序获取锁,非公平锁则允许新来的线程直接参与竞争。
最后,ReentrantLock 还支持 Condition,可以为同一把锁创建多个等待队列,比 synchronized 的 wait/notify 更灵活。
所以 ReentrantLock 的核心能力可以归纳为:
|
|
这些能力也是它相比 synchronized 更灵活的地方。
AQS
ReentrantLock 底层主要依赖 AQS,也就是 AbstractQueuedSynchronizer。
AQS 可以理解成 Java 并发包里的一套同步框架,ReentrantLock、Semaphore、CountDownLatch 等很多并发工具底层都会基于它实现。
对于 ReentrantLock 来说,AQS 主要依靠两个东西:
一个是 state,用来表示锁当前的状态;另一个是等待队列,用来管理没有获取到锁的线程。
当 state = 0 时,表示当前没有线程持有锁。线程获取锁时,会通过 CAS 尝试把:
|
|
如果成功,就表示获取锁成功,同时记录当前持锁线程。
如果锁已经被当前线程持有,那么再次获取时只需要增加 state,这也是 ReentrantLock 实现可重入的基础。
如果其他线程获取锁失败,就会进入 AQS 的等待队列,并通过 LockSupport.park() 挂起。等持锁线程释放锁以后,再由 AQS 唤醒等待线程重新参与竞争。
AQS 的等待队列
当线程获取锁失败以后,AQS 会把这些线程组织到一个等待队列中。
这个队列采用的是 CLH 队列的变体。
CLH 本身是一种用于组织竞争线程的队列思想,核心是让线程按照顺序排队,后面的线程关注前面的节点,而不是所有线程一直同时竞争同一个锁变量。
AQS 在这个思路上做了改造,使用 Node 组成一个双向链表:
|
|
其中 head 和 tail 分别指向队头和队尾,每个 Node 会保存当前线程、前驱节点、后继节点以及等待状态等信息。
线程获取锁失败以后,会被封装成一个 Node,然后通过 CAS 加到队尾。
加入队列以后,线程不会一直无休止地抢锁,而是根据前驱节点的状态决定是否继续尝试,或者通过 LockSupport.park() 挂起。
当持锁线程释放锁以后,AQS 会唤醒队列中靠前的等待线程,让它重新尝试获取锁。
如果获取成功,这个节点会成为新的 head,后面的线程继续等待。
所以 AQS 的等待队列可以理解成:
基于 CLH 思想实现的双向链表,用来有序管理获取锁失败的线程。
unlock 的执行过程
ReentrantLock 释放锁时,会调用 unlock()。
底层首先会减少 AQS 中的 state。
因为 ReentrantLock 支持可重入,所以只有当 state 最终减到 0 时,才表示这把锁真正被释放,同时清空当前持锁线程。
如果等待队列中还有其他线程,AQS 会唤醒后继等待节点,让它重新尝试获取锁。
所以释放锁的大致过程就是:
|
|
公平锁和非公平锁
ReentrantLock 可以选择公平锁或者非公平锁:
|
|
公平锁强调的是先来先得。
线程准备获取锁时,如果发现前面已经有其他线程在 AQS 队列中等待,就不会直接抢锁,而是先进入队列排队。
因此整体更接近:
|
|
非公平锁则允许新来的线程直接尝试 CAS 抢锁。
即使等待队列里已经有线程排队,新线程到来时仍然可以先尝试:
|
|
如果 CAS 恰好成功,它就可能直接拿到锁,相当于“插队”。
所以公平锁和非公平锁最核心的区别,并不是等待队列结构不同,而是:
获取锁之前,要不要先检查前面有没有等待线程。
公平锁会检查队列中的前驱节点,尽量按照等待顺序获取锁。
非公平锁则先直接抢一次,抢不到再进入 AQS 队列。
ReentrantLock 默认使用非公平锁,因为非公平锁通常吞吐量更高。刚释放锁的线程和新到来的线程可以直接竞争,减少线程频繁挂起和唤醒带来的切换成本。
公平锁的顺序更可控,但代价通常是更多的线程调度和上下文切换,因此吞吐量往往低一些。
Condition 的等待队列
Condition 用来处理这样一种场景:线程已经拿到了锁,但发现当前业务条件还不满足,于是主动释放锁,等待条件发生变化。
例如生产者拿到锁以后发现队列已经满了,这时候继续持有锁没有意义,因为消费者也需要拿到这把锁才能取走数据。
所以线程可以调用:
|
|
await() 会把当前线程加入这个 Condition 自己的等待队列,同时释放当前持有的 ReentrantLock,然后线程进入等待状态。
这里需要区分两种队列。
AQS 的同步队列是用来等待锁的,属于 CLH 队列的变体,本质上是双向链表。
而 Condition 有自己独立的等待队列,它不是 CLH 队列,主要通过节点的 nextWaiter 串起来,可以理解成一个单向链表。
所以两者分别表示:
|
|
当其他线程改变了业务条件以后,可以调用:
|
|
signal() 不会让等待线程立刻继续执行,而是把 Condition 等待队列中的第一个有效等待节点转移到 AQS 的同步队列中。
它接下来还需要重新参与锁竞争:
|
|
线程并不是从方法开头重新执行,而是从 await() 返回以后继续执行。
不过 await() 外面通常会使用 while:
|
|
因为 signal() 只代表“条件可能发生了变化”,并不能保证线程重新拿到锁时条件一定还满足,所以线程恢复以后需要再次检查条件。
如果调用的是:
|
|
那么会把当前 Condition 等待队列中的所有有效等待节点都转移到 AQS 同步队列中,之后它们再分别重新竞争锁。
因此,Condition 的完整流程可以理解成:
|
|
Condition 真正的价值在于,一把 ReentrantLock 可以创建多个独立的 Condition,从而把不同业务条件下等待的线程分开管理。
例如生产者消费者场景中,可以分别创建:
|
|
消费者在队列为空时等待 notEmpty,生产者在队列满时等待 notFull。
这样唤醒线程时,就可以只通知真正需要被唤醒的那一类线程。
ReentrantLock 和 synchronized 的区别
ReentrantLock 和 synchronized 都能实现互斥,并且都支持可重入,但它们的使用方式和能力不一样。
synchronized 是 Java 语言级的关键字,加锁和释放锁由 JVM 自动完成,写法更简单,也不容易因为忘记释放锁而出问题。
ReentrantLock 是一个显式的锁对象,需要手动调用:
|
|
所以使用时必须注意在 finally 中释放锁。
相比 synchronized,ReentrantLock 提供了更多控制能力,比如 tryLock()、超时获取、可中断获取、公平锁,以及多个 Condition 等待队列。
可以简单理解成:
|
|
如果只是普通的互斥同步,没有特殊需求,通常优先使用 synchronized,因为代码更简单,维护成本也更低。
如果业务需要下面这些能力,就更适合使用 ReentrantLock:
|
|
所以实际选择时,重点不是比较谁“更高级”,而是看业务是否需要额外的锁控制能力。
synchronized 适合简单、常规的同步场景;ReentrantLock 更适合需要精细控制锁行为的场景。