什么是可重入锁,如何实现可重入锁?
2026年01月15日
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
面试官问这个问题,核心是想考察你对以下方面的理解深度:
- 对锁基本概念的理解:你是否清晰理解 “可重入” 这个核心特性及其解决的问题。
- 对并发编程实践的掌握:可重入锁是避免死锁、编写健壮并发代码的关键工具之一。面试官想知道你是否理解其在实际编码中的应用场景和重要性。
- 对底层实现原理的探究能力:不仅仅是知道 “是什么”,更想知道你能否阐述其实现机制,这能体现你是否真正“吃透”了这个概念。
- 对 Java 并发体系的知识广度:能否联系
synchronized关键字和ReentrantLock类,说明 Java 中两种主要的可重入锁实现。
核心答案
可重入锁(Reentrant Lock)是一种特殊的线程同步机制,它允许同一个线程在已经持有该锁的情况下,可以多次成功获取同一把锁,而不会因为等待自己释放锁而导致死锁。
在 Java 中,synchronized 关键字和 java.util.concurrent.locks.ReentrantLock 类都是典型的可重入锁实现。
深度解析
原理/机制
可重入性的实现,核心在于为锁关联一个 持有线程标识 和一个 重入计数器。
- 初次加锁:当线程第一次请求锁时,JVM 或
ReentrantLock会记录当前持有锁的线程,并将计数器设置为 1。 - 重入加锁:同一个线程再次请求该锁时,系统会检查请求线程是否为当前持有线程。如果是,则直接将计数器加 1,并立即成功 “获取” 锁(实际上并未阻塞)。
- 释放锁:线程每次调用
unlock()或退出synchronized块时,计数器减 1。 - 完全释放:当计数器减到 0 时,锁才被真正释放,此时会清除持有线程标识,并唤醒其他等待该锁的线程。
这个机制避免了线程在递归调用、或一个同步方法调用另一个同步方法时,发生 “自己等自己” 的典型死锁场景。
代码示例
// 示例1:使用 synchronized(隐式可重入锁)演示递归重入
public class ReentrantDemo {
public synchronized void outer() {
System.out.println("进入 outer 方法,持有锁。");
inner(); // 递归调用另一个同步方法
System.out.println("退出 outer 方法。");
}
public synchronized void inner() {
System.out.println("进入 inner 方法,成功重入锁。");
// 如果锁不可重入,线程将在此处永久阻塞,等待自己释放锁(死锁)
}
public static void main(String[] args) {
ReentrantDemo demo = new ReentrantDemo();
demo.outer(); // 同一个线程,可以顺利执行
}
}
// 示例2:使用 ReentrantLock(显式可重入锁)演示重入计数
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock();
public void performAction() {
lock.lock(); // 第一次获取锁,计数=1
try {
System.out.println("首次加锁,重入计数: 1");
reentrantAction(); // 调用另一个也需要同一把锁的方法
} finally {
lock.unlock(); // 最终释放,计数归零
}
}
public void reentrantAction() {
lock.lock(); // 第二次获取(重入),计数=2
try {
System.out.println("重入加锁,此时锁被同一个线程持有,计数递增。");
} finally {
lock.unlock(); // 释放一次,计数=1
}
}
}
对比分析与常见误区
-
可重入锁 vs. 不可重入锁 | 特性 | 可重入锁 (如
synchronized,ReentrantLock) | 不可重入锁 (简单自旋锁的基础形态) | | :--- | :--- | :--- | | 同一线程重入 | 允许,不会死锁 | 不允许,会导致死锁 | | 实现复杂度 | 较高,需维护线程ID和计数器 | 较低,只维护锁状态 | | 性能影响 | 略有开销(维护计数器) | 无此开销,但实用性低 | | 适用场景 | 通用场景,尤其是存在递归、回调或复杂同步方法调用的 OOP 环境 | 极少数特殊场景,或用于教学理解锁的基本概念 | -
常见误区
- 误区一:可重入等于高并发/高性能。 错。可重入性解决的是正确性问题(避免自死锁),它本身不直接提升并发性能。
- 误区二:
synchronized不是可重入锁。 错。这是最容易混淆的点。从 JDK 1.0 开始,synchronized实现的监视器锁就是可重入的。 - 混淆“重入”与“并发”:可重入指的是 “同一个线程” 的重复进入,而非“多个线程”的并发访问。多个线程的并发安全仍需由锁的互斥性来保证。
最佳实践
- 优先使用内置机制:对于大多数同步场景,优先考虑使用
synchronized。JVM 会对其进行持续优化(如锁升级),且代码更简洁,不易出错(自动释放)。 - 按需选择
ReentrantLock:当需要尝试非阻塞获取锁(tryLock)、可中断的锁等待、公平锁策略、或绑定多个条件(Condition) 等高级功能时,再使用ReentrantLock。 - 确保释放:使用
ReentrantLock时,必须在finally块中调用unlock()以确保锁在任何情况下都能被释放,防止死锁。 - 理解重入深度:虽然重入避免了死锁,但过深的递归重入(如 Bug 导致的无限递归)最终会引发
StackOverflowError,而非死锁。
总结
可重入锁通过持有线程标识 + 重入计数器的机制,允许同一线程多次获取同一把锁,从根本上防止了线程自身导致的死锁,是 Java 并发编程中保证同步代码块或方法正确、灵活调用的基石。