什么是可重入锁,如何实现可重入锁?
一则或许对你有用的小广告
欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书
《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于
Spring AI + Spring Boot 3.x + JDK 21...,查看介绍《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/
新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍
截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观
面试考察点
-
概念理解:面试官不仅仅是想知道 "同一个线程可以重复获取同一把锁",更是想知道你是否理解 "为什么必须可重入"——如果锁不可重入,一个同步方法调用另一个同步方法会怎样?直接死锁。
-
实现原理:考察你是否清楚
synchronized通过Monitor计数器实现重入,ReentrantLock通过 AQS 的state+exclusiveOwnerThread实现重入。这两种实现方式的异同。 -
AQS 源码级理解:这是加分项。能说出
nonfairTryAcquire()中判断current == getExclusiveOwnerThread()的逻辑,说明你真的看过源码。
核心答案
先甩结论:可重入锁指的是同一个线程可以重复获取同一把锁而不会被阻塞。每次获取锁计数器 +1,每次释放计数器 -1,减到 0 才真正释放锁。Java 中 synchronized 和 ReentrantLock 都是可重入锁。
如果锁不可重入,一个同步方法调用另一个同步方法时,线程会永远等自己释放锁——也就是 自己锁死自己,经典的自锁死锁。
深度解析
一、为什么必须可重入?
先看一个日常开发中非常常见的场景:
class UserService {
public synchronized void updateUser() {
// 获取了 this 对象的锁
validateUser(); // 调用另一个 synchronized 方法
// ... 更新用户信息
}
public synchronized void validateUser() {
// 也需要获取 this 对象的锁
// 如果锁不可重入,这里就死锁了!
// 线程已经持有 this 锁,又来申请 this 锁,永远等不到
}
}
updateUser() 获取了 this 对象的锁,内部又调用了 validateUser(),也需要同一把锁。如果锁不可重入,线程就卡在自己手里了——它已经持有锁,但又申请同一把锁,没人能释放,直接死锁。
可重入锁解决的就是这个问题:同一个线程再次获取自己已持有的锁时,不会被阻塞,而是给计数器 +1。
二、synchronized 如何实现可重入?
synchronized 基于 JVM 的 Monitor(监视器锁)实现。每个对象关联一个 Monitor,其中有三个关键字段:
上图展示了 synchronized 的重入机制。核心逻辑很简单:
- 获取锁时:如果
_owner就是当前线程,说明是重入,直接_count++ - 释放锁时:
_count--,只有减到 0 时才真正释放(_owner = null)
这一切都是 JVM 底层自动完成的,对程序员完全透明。
三、ReentrantLock 如何实现可重入?
ReentrantLock 基于 AQS 实现,重入逻辑藏在 tryAcquire() 方法里。核心是两个东西:state 计数器 + exclusiveOwnerThread 持有者。
先看非公平锁的 tryAcquire() 源码(nonfairTryAcquire):
// ReentrantLock.Sync.nonfairTryAcquire()
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// state == 0,锁空闲,CAS 抢锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current); // 记录持有者
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// ⬆️ 关键!当前线程 == 锁持有者 → 可重入
int nextc = c + acquires; // state + 1
if (nextc < 0) // int 溢出检查
throw new Error("Maximum lock count exceeded");
setState(nextc); // 直接 set,不需要 CAS(只有持有者能走到这里)
return true;
}
return false; // 其他线程持有锁,获取失败
}
重入的核心就在 else if (current == getExclusiveOwnerThread()) 这一行:
- 如果
state != 0(锁被占用),先检查当前线程是不是锁的持有者 - 如果是,
state + 1,表示重入了一层,直接返回true(获取成功) - 不需要 CAS,因为只有持有锁的线程才能走到这个分支,没有竞争
再看释放锁的 tryRelease():
// ReentrantLock.Sync.tryRelease()
protected final boolean tryRelease(int releases) {
int c = getState() - releases; // state - 1
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException(); // 不是持有者不能释放
boolean free = false;
if (c == 0) {
// state 减到 0,真正释放锁
free = true;
setExclusiveOwnerThread(null); // 清除持有者
}
setState(c); // state > 0 说明还有重入,只减计数,不释放
return free;
}
释放逻辑也很清晰:state - 1,只有减到 0 才真正释放锁(清除 exclusiveOwnerThread),否则只是减少重入计数。
四、一个完整示例
ReentrantLock lock = new ReentrantLock();
lock.lock(); // 第一次获取:state = 1, owner = 当前线程
lock.lock(); // 重入:state = 2(同一个线程,直接 +1)
lock.lock(); // 再重入:state = 3
lock.unlock(); // state = 2(还没释放)
lock.unlock(); // state = 1(还没释放)
lock.unlock(); // state = 0,真正释放!owner = null
一个坑:lock() 和 unlock() 必须成对。多 lock 了一次少 unlock 了一次,锁就永远不会释放。所以 unlock() 一定要放 finally 块里:
lock.lock();
try {
lock.lock(); // 重入
try {
// 业务逻辑
} finally {
lock.unlock(); // 解除重入
}
} finally {
lock.unlock(); // 释放最外层锁
}
实际开发中更推荐写法是把 lock/unlock 放在方法边界,而不是嵌套:
public void methodA() {
lock.lock();
try {
methodB(); // methodB 内部也会 lock,但因为是可重入的所以不会死锁
} finally {
lock.unlock();
}
}
public void methodB() {
lock.lock();
try {
// 业务逻辑
} finally {
lock.unlock();
}
}
五、synchronized vs ReentrantLock 的可重入对比
| 维度 | synchronized |
ReentrantLock |
|---|---|---|
| 重入计数器 | _count(Monitor 字段) |
state(AQS 字段) |
| 持有者标识 | _owner(Monitor 字段) |
exclusiveOwnerThread(AQS 字段) |
| 计数上限 | 无明确上限 | Integer.MAX_VALUE(约 21 亿次) |
| 实现层 | JVM 底层(C++) | Java 层(AQS) |
| 可中断/超时 | ❌ | ✅ lockInterruptibly() / tryLock(timeout) |
| 公平性 | 非公平 | 支持公平/非公平 |
面试高频追问
-
可重入锁的最大重入次数是多少?
ReentrantLock的state是int类型,理论上最大 2^31 - 1(约 21 亿次)。synchronized的_count没有明确上限。正常业务中重入超过几十次都是代码设计有问题,不用担心溢出。 -
如果
lock()和unlock()不匹配会怎样?少调一次
unlock():锁永远不会释放,其他线程永远等待。多调一次unlock():tryRelease()会抛IllegalMonitorStateException,因为state已经是 0 了,当前线程又不是持有者。 -
ReentrantReadWriteLock的读锁也是可重入的吗?是的。读锁和写锁都支持重入。写锁重入时
state的低 16 位 +1,读锁重入时每个线程有自己的计数器(ThreadLocal记录),高 16 位记录总读锁数。
常见面试变体
- "
synchronized是可重入锁吗?" - "
ReentrantLock的可重入是怎么实现的?" - "为什么锁必须可重入?"
- "
ReentrantReadWriteLock的读写锁如何实现重入?"
记忆口诀
可重入:同一个线程可以重复加锁,计数器 +1/-1,减到 0 才释放
实现核心:两个东西——持有者(谁拿着锁)+ 计数器(重入了几次)
源码关键:current == getExclusiveOwnerThread() → 是持有者就 state++
总结
可重入锁的核心是 "同一个线程可以重复获取同一把锁",通过 持有者标识 + 计数器 实现。synchronized 用 Monitor 的 _owner + _count,ReentrantLock 用 AQS 的 exclusiveOwnerThread + state。面试时把 "为什么需要可重入"(避免自锁死锁)和 "怎么实现"(判断持有者 + 计数器递增递减)讲清楚,再配上 nonfairTryAcquire() 的源码解读,这道题就是高分。
