01
60 秒回答模板
可重入锁指同一个线程在已经持有某把锁的情况下,可以再次获取这把锁而不会发生自我阻塞。它的实现关键一般有三个要素:第一,记录当前锁的 owner 线程;第二,用一个 state 或计数器记录重入次数;第三,释放锁时必须按获取次数对称释放。以 ReentrantLock 为例,它基于 AQS 实现,state 表示锁的持有次数。线程第一次加锁成功时,CAS 将 state 从 0 改为 1,并把 exclusiveOwnerThread 设置为当前线程;如果当前线程再次加锁,发现 owner 就是自己,则直接将 state 加一;如果其他线程来加锁,发现 state 不为 0 且 owner 不是自己,就进入 AQS 同步队列阻塞等待。解锁时,当前线程把 state 减一,只有减到 0 时才清空 owner,并唤醒队列中的后继线程。synchronized 也具备可重入性,只是它由 JVM 的对象监视器、锁记录和计数机制完成,开发者不能像 ReentrantLock 那样选择公平锁、响应中断、尝试获取锁或使用多个 Condition。可重入锁解决的是同一线程递归调用、方法嵌套调用时的自我死锁问题,但它不能消除多个线程之间的循环等待,因此仍然需要控制加锁顺序、锁粒度和释放路径。
考点 重入的含义
主线 owner 线程是判断重入的关键
易错点 把可重入误解成多个线程可以同时进入同一临界区。
02
深入解析
01 重入的含义
重入不是多个线程可以同时进入临界区,而是同一个线程可以重复进入同一把锁保护的代码。例如一个 public 方法加锁后调用另一个同样需要这把锁的 private 方法,如果锁不可重入,线程会在第二次获取同一把锁时等待自己释放锁,形成自我死锁。可重入锁允许这种嵌套调用,因为它识别出第二次请求锁的线程就是当前持有者,于是不会阻塞,而是增加持有次数。这个设计非常适合递归、模板方法、分层服务调用、回调链路等场景。
02 owner 线程是判断重入的关键
可重入锁必须知道锁当前属于哪个线程。只有记录 owner,才能区分持锁线程再次进入和其他线程竞争进入。如果 state 不为 0,但 owner 是当前线程,说明这是重入,可以直接成功;如果 owner 不是当前线程,说明锁被别人持有,当前线程必须等待、失败返回或按策略进入队列。没有 owner 信息,只靠一个布尔值表示锁是否被占用,就无法实现安全的重入语义。
03 state 或计数器维护重入层数
重入锁不是第二次进入时什么都不做,而是要记录进入了几层。第一次获取锁通常把计数从 0 改为 1,第二次重入变成 2,第三次变成 3。每次 unlock 或退出同步块时计数减一,只有减到 0 才代表当前线程完全离开临界区,锁才真正可被其他线程获取。这个计数器保证了外层逻辑还没有执行完时,内层释放不会提前放开锁,否则会破坏临界区的互斥性。
04 AQS 中 ReentrantLock 的实现思路
ReentrantLock 的核心依赖 AQS。AQS 用一个 int 类型的 state 表示同步状态,在独占锁语义下可以理解为持有次数。非公平锁加锁时通常先尝试 CAS 抢占,把 state 从 0 改成 1,成功后设置 owner 为当前线程;如果失败,再判断当前线程是否已经是 owner,是则 state 加一;否则进入 AQS 的 CLH 变体同步队列,线程可能被 park 挂起。释放锁时校验当前线程必须是 owner,然后 state 减少;当 state 归零,清空 owner,并通过 AQS 唤醒后继节点参与竞争。
05 synchronized 的可重入机制
synchronized 也是可重入锁。它的锁对象可以是普通对象、Class 对象或实例方法对应的 this。JVM 会在对象监视器或锁记录中维护持有线程和重入次数等信息。当同一个线程进入同一对象的另一个 synchronized 代码块或方法时,不会被阻塞,而是增加重入深度;退出时逐层减少。和 ReentrantLock 相比,synchronized 的优点是语义简单、自动释放、异常路径安全,缺点是控制能力较弱,不能手动选择公平策略,也不能显式创建多个条件队列。
06 释放次数必须与获取次数匹配
可重入锁最容易被忽略的一点是:获取几次就必须释放几次。对于 synchronized,JVM 会在代码块或方法退出时自动释放对应层级;对于 ReentrantLock,必须在 finally 中调用 unlock,且每次 lock 都要有对应的 unlock。如果 lock 两次只 unlock 一次,state 仍然大于 0,owner 不会清空,其他线程会一直无法获取这把锁。反过来,如果没有持有锁却 unlock,ReentrantLock 会抛出异常,因为非 owner 线程不能释放别人的锁。
07 公平锁与非公平锁
ReentrantLock 可以构造公平锁或非公平锁。公平锁在竞争时更尊重等待队列顺序,通常会检查前面是否已有等待线程,尽量让排队更久的线程先获得锁;非公平锁允许新来的线程直接尝试抢占,可能插队成功。非公平锁吞吐量通常更高,因为减少了严格排队带来的上下文切换和调度成本;公平锁更适合对等待时间有明确要求的场景,但不能保证绝对实时,只是降低长期饥饿的概率。
08 Condition 与等待队列
ReentrantLock 相比 synchronized 的一个重要扩展是 Condition。一个 ReentrantLock 可以创建多个 Condition,每个 Condition 都有独立的条件等待队列,这比 synchronized 只能配合 wait、notify、notifyAll 使用对象监视器的单一等待集合更灵活。线程调用 await 时会释放当前持有的锁并进入条件队列,收到 signal 后转移到同步队列中重新竞争锁,重新获得锁后才能继续执行。由于锁是可重入的,await 在释放和恢复时还需要处理原来的重入层数,保证线程醒来后持锁状态与进入等待前匹配。
09 死锁与使用边界
可重入锁只解决同一线程重复请求同一把锁导致的自我阻塞问题,并不自动解决多线程之间的死锁。两个线程分别持有 A 锁和 B 锁,然后互相等待对方释放,仍然会死锁。使用可重入锁时要控制锁顺序,缩小锁范围,避免持锁执行慢 IO、远程调用、复杂回调或不可控用户代码。需要超时、可中断、尝试获取、多条件队列时,ReentrantLock 更合适;只需要简单互斥且希望异常自动释放时,synchronized 通常更简洁。
03
易错点
- 把可重入误解成多个线程可以同时进入同一临界区。
- 只说同一个线程可以再次获取锁,但没有解释 owner 和 state 如何配合。
- 认为重入后释放一次就能完全释放锁,忽略获取次数和释放次数必须匹配。
- 使用 ReentrantLock 时没有在 finally 中 unlock,异常路径导致锁无法释放。
- 认为公平锁一定性能更好,忽略公平性和吞吐量之间的取舍。
- 认为可重入锁可以解决所有死锁问题,忽略多锁循环等待仍然存在。
- 混淆 synchronized 和 ReentrantLock,忽略前者由 JVM 自动管理、后者基于 AQS 并需要手动释放。
- 讲 Condition 时只说它类似 wait/notify,没有说明它有独立条件队列以及 await 会释放并重新竞争锁。
04
面试官追问
为什么可重入锁需要记录 owner?
因为锁需要区分当前请求来自持锁线程还是其他线程。state 不为 0 时,如果 owner 是当前线程,就属于重入,可以增加计数后继续执行;如果 owner 不是当前线程,就必须阻塞、排队或获取失败。
ReentrantLock 的 state 表示什么?
在独占锁场景下,state 表示锁的持有次数。0 表示未被持有,1 表示被某个线程持有一次,大于 1 表示同一线程发生了多层重入。
为什么 unlock 要放在 finally 里?
因为 ReentrantLock 不会像 synchronized 那样在作用域退出时自动释放。如果业务代码抛异常但没有执行 unlock,state 无法归零,owner 不会清空,其他线程可能永久等待。
公平锁一定比非公平锁好吗?
不一定。公平锁更强调排队顺序,能降低饥饿风险,但通常吞吐量较低;非公平锁允许插队竞争,吞吐量往往更好,但在极端竞争下可能让某些线程等待更久。
synchronized 是否可重入?
是。一个线程进入某个 synchronized 方法后,再调用同一对象上的另一个 synchronized 方法,不会被自己阻塞。JVM 会维护该线程对同一监视器的重入层数。
Condition 和 Object.wait 有什么核心差异?
Condition 依附于 ReentrantLock,一个锁可以有多个 Condition,因此可以把不同等待条件拆成不同队列;Object.wait 依附于对象监视器,表达能力相对粗。Condition 还要求线程先持有对应的 ReentrantLock。
可重入锁会不会导致死锁?
可重入锁本身不等于死锁,但错误使用仍会死锁。例如多个线程以不同顺序获取多把锁,或 lock 后没有正确 unlock,都可能造成长期等待。