文章目录
- 先说结论:可重入锁的核心要点
- 没有可重入锁会怎样?一个自我死锁的灾难
- 可重入锁是怎么实现的?计数器 + 线程判断
- synchronized 的可重入:JVM 层面天然支持
- 可重入锁的注意事项
- 可重入锁全景
- 回答技巧与点评
- 标准回答
- 加分回答
- 面试官点评
个人网站
你有没有写过这样的代码——方法 A 加了synchronized,方法 B 也加了synchronized,然后 A 调用了 B。按理说,A 已经拿到锁了,再去拿同一把锁,会不会把自己锁死?
如果锁不可重入,A 拿着锁等自己释放锁——这就是死锁。但神奇的是,程序跑得好好的,并没有卡住。这就是可重入锁在默默保护你。
先说结论:可重入锁的核心要点
| 维度 | 说明 |
|---|---|
| 什么是可重入 | 同一线程可以重复获取同一把锁,不会死锁 |
| 实现原理 | 持锁线程 ID 判断 + 计数器(state) |
| 每次加锁 | state +1 |
| 每次解锁 | state -1 |
| 完全释放 | state 减到 0 才真正释放锁 |
| synchronized | 天然可重入(JVM 实现) |
| ReentrantLock | 可重入(AQS 的 state 计数) |
| 不可重入的后果 | 同一线程重入时自己锁死自己 |
一句话记住:可重入锁就像你家门锁——你在屋里,再推内门不用重新开锁;不可重入就是你把自己锁在卧室了。
没有可重入锁会怎样?一个自我死锁的灾难
假设锁不可重入,看这段代码:
publicsynchronizedvoidmethodA(){methodB();// A 已持有 this 锁,调用 B 还要拿 this 锁 👈}publicsynchronizedvoidmethodB(){// 如果锁不可重入,这里会永远等待 → 自己死锁!}方法 A 拿到this对象的锁,调用方法 B,方法 B 也要拿this的锁。如果锁不可重入,线程 A 会等自己释放锁——左手等右手,永远等不到。
生活类比:你进了家门,卧室门也要用同一把钥匙。如果门锁"不可重入",你进了家门,再想进卧室——对不起,钥匙被你自己拿着呢,你得先"释放"家门锁才能开卧室门。但你人在家里,家门锁怎么释放?死锁了。
可重入锁就是解决这个问题:你已经拿过这把锁了,再拿一次直接放行,不用等。
可重入锁是怎么实现的?计数器 + 线程判断
可重入锁的核心就两步判断:
- 当前线程是不是已经持有这把锁?→ 是的话直接放行
- 计数器 +1,表示又重入了一次
以ReentrantLock为例,它基于 AQS 实现:
// AQS 内部(简化)finalbooleannonfairTryAcquire(intacquires){intc=getState();// 获取当前 stateif(c==0){// 锁空闲,CAS 抢锁 👈if(compareAndSetState(0,acquires))setExclusiveOwnerThread(Thread.currentThread());}elseif(getExclusiveOwnerThread()==Thread.currentThread()){// 同一线程重入 👈 state 累加setState(c+acquires);returntrue;}returnfalse;}关键逻辑:判断当前线程是否是持锁线程。如果是,state + 1,直接放行。
释放锁时反过来:
protectedfinalbooleantryRelease(intreleases){intc=getState()-releases;// state - 1 👈if(c==0){setExclusiveOwnerThread(null);// state 为 0 才真正释放 👈returntrue;}setState(c);returnfalse;}state 减到 0 才真正释放锁。加了 3 次锁,就得解锁 3 次。
synchronized 的可重入:JVM 层面天然支持
synchronized的可重入不需要你操心,JVM 在底层就搞定了。每个对象头里有个 Monitor,内部有计数器_count,逻辑和ReentrantLock一模一样:
- 同一线程再进
synchronized→ 计数器 +1 - 退出
synchronized→ 计数器 -1 - 计数器归零 → 释放 Monitor
所以你写synchronized嵌套调用,从来不用担心自己死锁自己——JVM 帮你兜底。
可重入锁的注意事项
可重入锁虽然好用,但有几个坑:
1. 加几次锁,就得解几次锁
lock.lock();lock.lock();// state = 2lock.unlock();// state = 1,锁还没释放!👈// 必须再 unlock() 一次,state 才归零lock.unlock();// state = 0,锁真正释放2. 子类重入父类的 synchronized 方法
classParent{publicsynchronizedvoiddoSomething(){/* ... */}}classChildextendsParent{@OverridepublicsynchronizedvoiddoSomething(){super.doSomething();// 重入同一把锁(this)👈}}子类调用super.doSomething()是重入,不会死锁。但如果子类和父类用不同的锁对象,那就不是重入了。
可重入锁全景
可重入锁 全景 核心机制 ├── 线程判断 ── 当前线程 == 持锁线程 → 放行 ├── 计数器 ── state 记录重入次数 │ ├── 加锁 state +1 │ └── 解锁 state -1,归零才释放 └── 两个实现 ├── synchronized ── JVM Monitor 计数,天然可重入 └── ReentrantLock ── AQS state 计数,显式可重入 注意事项 ├── 加几次锁,解几次锁 ├── 子类调父类 synchronized 是重入 └── 不同锁对象不算重入 口诀:同线程可重入,计数器来帮忙, 加几次解几次,归零才是释放。回答技巧与点评
标准回答
可重入锁是指同一线程可以重复获取同一把锁而不会死锁。实现原理是通过线程 ID 判断和计数器:持锁线程再次获取锁时,计数器加 1 而非阻塞;释放锁时计数器减 1,减到 0 才真正释放。synchronized 天然可重入(JVM Monitor 计数),ReentrantLock 也支持可重入(AQS 的 state 计数)。可重入锁的作用是避免同一线程在嵌套调用时自我死锁。
加分回答
- 设计原则:可重入锁体现了"最小意外"原则——如果同一线程已经持有锁,再次获取应该是自然行为而非死锁。几乎所有主流锁实现都支持可重入,不可重入锁属于特殊场景
- 边界情况:
ReentrantLock的lock()和unlock()必须成对出现,嵌套几次就 unlock 几次。如果只 unlock 一次,锁不会释放,其他线程永远等待——这是synchronized不会出的问题 - 实际应用:框架中大量使用可重入特性。比如 Spring 的事务管理器在
@Transactional嵌套调用时,同一线程可以重复获取数据库连接锁;AQS 的acquire()方法本身也是可重入设计
面试官点评
这道题考的是你对锁机制的底层理解。只说"可重入就是能重复加锁"太浅了。能讲清楚 state 计数器的实现、为什么必须减到零才释放、以及 synchronized 和 ReentrantLock 在可重入上的不同实现路径,才能拿高分。如果你能指出"可重入是锁的基本能力,不是高级特性",面试官会认为你理解到位。
原文阅读
内容有帮助?点赞、收藏、关注三连!评论区等你 💪