可重入锁详解(从零理解)
目录
1. 什么是可重入锁
1.1 生活类比(最重要!)
场景:你家的大门
不可重入锁(普通锁):
1. 你拿钥匙打开家门,进入客厅
2. 你想从客厅去厨房(需要再次经过门)
3. 但是!门已经被你锁上了
4. 你在门内,但无法再次"开门"进入
5. 结果:你把自己锁在里面,进退两难!❌
可重入锁(智能锁):
1. 你拿钥匙打开家门,进入客厅(锁记录:你进来了,计数=1)
2. 你想从客厅去厨房,再次经过门
3. 锁识别:"咦,是同一个人(你)!"
4. 锁自动让你通过(计数=2)
5. 你从厨房出来(计数=1)
6. 你最终离开家(计数=0,真正解锁)✅
关键点:
- 锁能识别"持有者"是谁
- 同一个人可以多次进入
- 记录进入的次数
- 必须同样次数的"出门"才能真正解锁
1.2 编程角度
可重入锁 = Reentrant Lock
定义:
同一个线程在持有锁的情况下,可以再次获取同一把锁,不会被自己阻塞。
核心特征:
1. 识别线程身份(谁持有锁?)
2. 计数器(获取了多少次?)
3. 匹配释放(获取N次,必须释放N次)
1.3 图解说明
不可重入锁:
线程A
↓
lock.lock() ✅ 第一次获取成功
↓
执行业务代码
↓
调用其他方法
↓
lock.lock() ❌ 再次获取,阻塞!(自己锁住自己)
↓
💀 死锁!永远等待
可重入锁:
线程A
↓
lock.lock() ✅ 第一次获取成功(计数器=1)
↓
执行业务代码
↓
调用其他方法
↓
lock.lock() ✅ 再次获取成功(计数器=2,同一线程)
↓
继续执行
↓
lock.unlock() (计数器=1)
↓
lock.unlock() (计数器=0,真正释放)
↓
✅ 执行完成
2. 为什么需要可重入锁
2.1 问题场景:递归方法+方法调用链
正是因为 synchronized 是可重入锁,这个递归才得以执行。
java
// 场景1:递归方法
public synchronized void recursiveMethod(int n) {
System.out.println("执行第 " + n + " 层");
if (n < 5) {
recursiveMethod(n + 1); // 递归调用自己
}
}
// 如果不是可重入锁:
线程调用 recursiveMethod(1)
→ 获取锁(第1层)
→ 调用 recursiveMethod(2)
→ 尝试获取锁(第2层)❌ 被第1层的锁阻塞
→ 💀 死锁!
// 因为是可重入锁(synchronized 是可重入的):
线程调用 recursiveMethod(1)
→ 获取锁(计数器=1)
→ 调用 recursiveMethod(2)
→ 获取锁(计数器=2,同一线程)✅
→ 调用 recursiveMethod(3)
→ 获取锁(计数器=3)✅
→ ...
→ 逐层返回,逐层释放
→ ✅ 执行成功
java
// 场景2:方法调用链
在一个函数里调用另外一个函数,都涉及到了可重入锁才是成功的,否则还说会自己锁住自己。
public class BankAccount {
private int balance = 1000;
// 方法1:取款
public synchronized void withdraw(int amount) {
if (balance >= amount) {
balance -= amount;
log("取款 " + amount); // 调用 log 方法
}
}
// 方法2:记录日志(也需要加锁)
public synchronized void log(String msg) {
System.out.println("[" + Thread.currentThread().getName() + "] " + msg);
}
}
// 调用流程:
线程A 调用 withdraw(100)
→ 获取 synchronized 锁(计数器=1)
→ 执行 balance -= amount
→ 调用 log("取款 100")
→ log 方法也是 synchronized,尝试获取锁
// 如果不是可重入锁:
→ ❌ 被阻塞(锁已经被自己持有)
→ 💀 死锁!
// 因为是可重入锁:
→ ✅ 再次获取锁成功(计数器=2,同一线程)
→ 执行 log 方法
→ log 方法返回(计数器=1)
→ withdraw 方法返回(计数器=0,真正释放)
→ ✅ 执行成功
2.2 为什么方法调用链需要可重入?
现实开发中的常见情况:
方法A(加锁)
↓
调用 方法B(加锁)
↓
调用 方法C(加锁)
如果不是可重入锁:
- 方法A 获取锁
- 方法B 尝试获取锁 → 被方法A 阻塞
- 💀 死锁
如果是可重入锁:
- 方法A 获取锁(计数=1)
- 方法B 获取锁(计数=2)
- 方法C 获取锁(计数=3)
- 逐层返回,逐层释放
- ✅ 正常执行
3. 不可重入锁的问题
3.1 自己锁住自己(死锁)
java
// 模拟一个不可重入锁
public class NonReentrantLock {
private boolean isLocked = false;
public synchronized void lock() {
while (isLocked) {
try {
wait(); // 锁被占用,等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
isLocked = true; // 获取锁
}
public synchronized void unlock() {
isLocked = false;
notify(); // 唤醒等待的线程
}
}
// 使用不可重入锁的问题:
public class DeadlockExample {
private NonReentrantLock lock = new NonReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
display(); // 调用另一个方法
} finally {
lock.unlock();
}
}
public void display() {
lock.lock(); // ❌ 尝试再次获取锁
try {
System.out.println("Count: " + count);
} finally {
lock.unlock();
}
}
}
// 执行 increment() 时:
线程A
↓
lock.lock() in increment() ✅ 获取锁成功
↓
count++
↓
调用 display()
↓
lock.lock() in display() ❌ 锁已被占用(被自己占用!)
↓
进入 wait() 状态,等待锁释放
↓
但是!锁要在 increment() 方法结束时才释放
而 increment() 方法在等待 display() 返回
display() 在等待锁释放
↓
💀 死锁!线程永远等待
3.2 代码示例对比
java
// ========== 不可重入锁的问题 ==========
public class NonReentrantExample {
// 假设这是一个不可重入锁
private final Object lock = new Object();
private int count = 0;
public void method1() {
synchronized (lock) { // 第一次获取锁
System.out.println("Method1");
method2(); // 调用 method2
}
}
public void method2() {
synchronized (lock) { // 第二次获取锁(同一线程)
System.out.println("Method2"); // ❌ 如果不可重入,这里会死锁
}
}
public static void main(String[] args) {
NonReentrantExample example = new NonReentrantExample();
example.method1(); // 如果 synchronized 不可重入,会死锁
}
}
// ========== 可重入锁的正常工作 ==========
public class ReentrantExample {
private int count = 0;
// synchronized 是可重入的
public synchronized void method1() {
System.out.println("Method1 - 获取锁(计数=1)");
method2(); // 调用 method2
System.out.println("Method1 - 释放锁(计数=0)");
}
public synchronized void method2() {
System.out.println("Method2 - 获取锁(计数=2)");
count++;
System.out.println("Method2 - 释放锁(计数=1)");
}
public static void main(String[] args) {
ReentrantExample example = new ReentrantExample();
example.method1(); // ✅ 正常执行
}
}
// 输出:
// Method1 - 获取锁(计数=1)
// Method2 - 获取锁(计数=2)
// Method2 - 释放锁(计数=1)
// Method1 - 释放锁(计数=0)
4. 可重入锁的实现原理
4.1 核心数据结构
java
// 可重入锁的关键信息
class ReentrantLockState {
// 1. 当前持有锁的线程
private Thread owner;
// 2. 重入计数器
private int count;
}
// 工作流程:
初始状态:
owner = null
count = 0
线程A 第一次获取锁:
owner = 线程A
count = 1
线程A 第二次获取锁(重入):
if (当前线程 == owner) {
count = 2 // 计数器+1
return true; // 获取成功
}
线程B 尝试获取锁:
if (当前线程 != owner) {
return false; // 获取失败,等待
}
线程A 第一次释放锁:
count = 1 // 计数器-1
// 锁未真正释放(count > 0)
线程A 第二次释放锁:
count = 0 // 计数器-1
owner = null // 清除持有者
// 锁真正释放,唤醒等待的线程
4.2 简化实现 ReentrantLock
java
public class SimpleReentrantLock {
// 持有锁的线程
private Thread owner;
// 重入次数
private int holdCount = 0;
// 获取锁
public synchronized void lock() {
Thread currentThread = Thread.currentThread();
// 情况1:锁空闲
if (owner == null) {
owner = currentThread;
holdCount = 1;
System.out.println(currentThread.getName() + " 首次获取锁");
return;
}
// 情况2:当前线程已持有锁(重入)
if (owner == currentThread) {
holdCount++;
System.out.println(currentThread.getName() + " 重入锁,计数=" + holdCount);
return;
}
// 情况3:其他线程持有锁,等待
while (owner != null) {
try {
System.out.println(currentThread.getName() + " 等待锁...");
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 被唤醒后,获取锁
owner = currentThread;
holdCount = 1;
System.out.println(currentThread.getName() + " 获取锁");
}
// 释放锁
public synchronized void unlock() {
Thread currentThread = Thread.currentThread();
// 只有持有锁的线程才能释放
if (owner != currentThread) {
throw new IllegalMonitorStateException("当前线程未持有锁");
}
holdCount--;
System.out.println(currentThread.getName() + " 释放锁,计数=" + holdCount);
// 计数器归零,真正释放锁
if (holdCount == 0) {
owner = null;
System.out.println(currentThread.getName() + " 完全释放锁");
notify(); // 唤醒一个等待的线程
}
}
}
4.3 测试可重入性
java
public class ReentrantTest {
private SimpleReentrantLock lock = new SimpleReentrantLock();
public void methodA() {
lock.lock();
try {
System.out.println("执行 methodA");
methodB(); // 调用 methodB
} finally {
lock.unlock();
}
}
public void methodB() {
lock.lock(); // 再次获取锁(重入)
try {
System.out.println("执行 methodB");
methodC(); // 调用 methodC
} finally {
lock.unlock();
}
}
public void methodC() {
lock.lock(); // 再次获取锁(重入)
try {
System.out.println("执行 methodC");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantTest test = new ReentrantTest();
test.methodA();
}
}
// 输出:
// Thread-0 首次获取锁
// 执行 methodA
// Thread-0 重入锁,计数=2
// 执行 methodB
// Thread-0 重入锁,计数=3
// 执行 methodC
// Thread-0 释放锁,计数=2
// Thread-0 释放锁,计数=1
// Thread-0 释放锁,计数=0
// Thread-0 完全释放锁
5. 代码示例
5.1 递归方法(最经典)
java
public class RecursiveExample {
private int count = 0;
// synchronized 是可重入的
public synchronized void recursiveMethod(int n) {
count++;
System.out.println("第 " + n + " 层递归,count = " + count);
if (n < 5) {
recursiveMethod(n + 1); // 递归调用,再次获取锁
}
System.out.println("第 " + n + " 层返回");
}
public static void main(String[] args) {
RecursiveExample example = new RecursiveExample();
example.recursiveMethod(1);
System.out.println("最终 count = " + example.count);
}
}
// 输出:
// 第 1 层递归,count = 1 (获取锁,计数=1)
// 第 2 层递归,count = 2 (重入锁,计数=2)
// 第 3 层递归,count = 3 (重入锁,计数=3)
// 第 4 层递归,count = 4 (重入锁,计数=4)
// 第 5 层递归,count = 5 (重入锁,计数=5)
// 第 5 层返回 (释放锁,计数=4)
// 第 4 层返回 (释放锁,计数=3)
// 第 3 层返回 (释放锁,计数=2)
// 第 2 层返回 (释放锁,计数=1)
// 第 1 层返回 (释放锁,计数=0,真正释放)
// 最终 count = 5
5.2 方法调用链
java
public class MethodChainExample {
private List<String> logs = new ArrayList<>();
// 主业务方法
public synchronized void processOrder(Order order) {
System.out.println("开始处理订单");
// 1. 验证订单
validateOrder(order);
// 2. 扣减库存
deductStock(order);
// 3. 记录日志
log("订单处理完成: " + order.getId());
System.out.println("订单处理结束");
}
// 验证方法(也需要加锁)
public synchronized void validateOrder(Order order) {
System.out.println(" 验证订单");
// 验证逻辑...
log("订单验证通过: " + order.getId());
}
// 扣减库存方法(也需要加锁)
public synchronized void deductStock(Order order) {
System.out.println(" 扣减库存");
// 扣减逻辑...
log("库存扣减成功: " + order.getProductId());
}
// 日志方法(也需要加锁)
public synchronized void log(String message) {
System.out.println(" [LOG] " + message);
logs.add(message);
}
public static void main(String[] args) {
MethodChainExample example = new MethodChainExample();
Order order = new Order("ORDER001", "PRODUCT001");
example.processOrder(order);
}
static class Order {
private String id;
private String productId;
public Order(String id, String productId) {
this.id = id;
this.productId = productId;
}
public String getId() { return id; }
public String getProductId() { return productId; }
}
}
// 执行流程:
// processOrder() 获取锁(计数=1)
// ↓
// validateOrder() 重入锁(计数=2)
// ↓
// log() 重入锁(计数=3)
// log() 释放(计数=2)
// ↓
// validateOrder() 释放(计数=1)
// ↓
// deductStock() 重入锁(计数=2)
// ↓
// log() 重入锁(计数=3)
// log() 释放(计数=2)
// ↓
// deductStock() 释放(计数=1)
// ↓
// log() 重入锁(计数=2)
// log() 释放(计数=1)
// ↓
// processOrder() 释放(计数=0,真正释放)
5.3 ReentrantLock 示例
java
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private ReentrantLock lock = new ReentrantLock();
private int balance = 1000;
public void withdraw(int amount) {
lock.lock(); // 第一次获取锁
try {
System.out.println("开始取款: " + amount);
if (checkBalance(amount)) { // 调用另一个需要锁的方法
balance -= amount;
System.out.println("取款成功,余额: " + balance);
} else {
System.out.println("余额不足");
}
} finally {
lock.unlock(); // 释放锁
}
}
public boolean checkBalance(int amount) {
lock.lock(); // 第二次获取锁(重入)
try {
System.out.println(" 检查余额,当前余额: " + balance);
System.out.println(" 锁的持有次数: " + lock.getHoldCount()); // 显示重入次数
return balance >= amount;
} finally {
lock.unlock(); // 释放锁
}
}
public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
example.withdraw(500);
}
}
// 输出:
// 开始取款: 500
// 检查余额,当前余额: 1000
// 锁的持有次数: 2 ← 重入了2次!
// 取款成功,余额: 500
5.4 对比:不可重入锁导致死锁
java
// 错误示例:模拟不可重入锁导致的死锁
public class NonReentrantDeadlock {
// 假设这是一个不可重入的锁
private final Object lock = new Object();
public void outerMethod() {
synchronized (lock) {
System.out.println("Outer method");
innerMethod(); // 尝试调用 innerMethod
}
}
public void innerMethod() {
// 如果 synchronized 不可重入,这里会永远阻塞
synchronized (lock) { // ❌ 尝试获取已被占用的锁
System.out.println("Inner method");
}
}
}
// 幸运的是,Java 的 synchronized 是可重入的,所以不会死锁
// 但如果你自己实现一个不可重入的锁,就会出现这个问题
6. 常见场景
6.1 场景 1:递归树遍历
java
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
public TreeNode(int val) {
this.val = val;
}
}
public class TreeTraversal {
private List<Integer> result = new ArrayList<>();
// 递归遍历树(需要加锁保护 result)
public synchronized void traverse(TreeNode node) {
if (node == null) {
return;
}
result.add(node.val);
// 递归调用(重入锁)
traverse(node.left);
traverse(node.right);
}
public List<Integer> getResult() {
return result;
}
}
6.2 场景 2:嵌套事务
java
public class TransactionManager {
private ReentrantLock lock = new ReentrantLock();
// 外层事务
public void outerTransaction() {
lock.lock();
try {
System.out.println("外层事务开始");
// 执行业务逻辑1
updateDatabase("table1", "data1");
// 调用内层事务
innerTransaction();
System.out.println("外层事务提交");
} finally {
lock.unlock();
}
}
// 内层事务(可以被外层事务调用)
public void innerTransaction() {
lock.lock(); // 重入锁
try {
System.out.println(" 内层事务开始");
// 执行业务逻辑2
updateDatabase("table2", "data2");
System.out.println(" 内层事务提交");
} finally {
lock.unlock();
}
}
private void updateDatabase(String table, String data) {
// 数据库更新逻辑
System.out.println(" 更新 " + table + ": " + data);
}
}
6.3 场景 3:缓存操作
java
public class Cache {
private Map<String, Object> cache = new HashMap<>();
private ReentrantLock lock = new ReentrantLock();
// 获取缓存,如果没有则从数据库加载
public Object get(String key) {
lock.lock();
try {
if (cache.containsKey(key)) {
return cache.get(key);
}
// 缓存未命中,从数据库加载
Object value = loadFromDatabase(key);
// 加载后放入缓存(调用 put 方法,重入锁)
put(key, value);
return value;
} finally {
lock.unlock();
}
}
// 放入缓存
public void put(String key, Object value) {
lock.lock(); // 重入锁
try {
cache.put(key, value);
System.out.println("缓存更新: " + key);
} finally {
lock.unlock();
}
}
private Object loadFromDatabase(String key) {
System.out.println("从数据库加载: " + key);
return "value_" + key;
}
}
7. 面试要点
7.1 标准回答模板
面试官:什么是可重入锁?
回答(分层次):
1. 定义(是什么):
可重入锁是指同一线程在持有锁的情况下,可以再次获取同一把锁,
而不会被自己阻塞。
2. 为什么需要(解决什么问题):
在递归调用或方法调用链中,如果方法都需要加锁,
不可重入锁会导致线程自己锁住自己,造成死锁。
3. 实现原理:
锁内部维护两个关键信息:
- 锁的持有线程(owner)
- 重入计数器(holdCount)
当同一线程再次获取锁时,计数器+1;
释放锁时,计数器-1;
只有计数器归零时,锁才真正释放。
4. Java 实现:
- synchronized:天然支持可重入
- ReentrantLock:显式的可重入锁实现
5. 举例:
(给出递归或方法调用链的例子)
7.2 关键问题
Q1:synchronized 是可重入的吗?
✅ 是的,synchronized 天然支持可重入。
证明:
public synchronized void method1() {
method2(); // 调用另一个 synchronized 方法
}
public synchronized void method2() {
// 如果不可重入,这里会死锁
// 但实际可以正常执行
}
Q2:为什么需要计数器?
原因:同一线程可能多次获取锁,必须相同次数的释放才能真正解锁。
例子:
lock(); // 计数=1
lock(); // 计数=2
lock(); // 计数=3
unlock(); // 计数=2(锁未释放)
unlock(); // 计数=1(锁未释放)
unlock(); // 计数=0(锁真正释放)
如果没有计数器:
- 第一次 unlock() 就会释放锁
- 但后续代码还认为持有锁
- 其他线程可能介入,造成数据不一致
Q3:可重入锁会有性能问题吗?
有,但很小:
性能开销:
1. 线程ID比较(当前线程 == 持有者?)
2. 计数器+1 或 -1
但相比于:
- 避免死锁的价值
- 代码编写的便利性
这点开销是完全可以接受的。
Q4:ReentrantLock 比 synchronized 好在哪?
ReentrantLock 的额外功能:
1. 可中断获取锁
lock.lockInterruptibly();
2. 超时获取锁
lock.tryLock(3, TimeUnit.SECONDS);
3. 公平锁/非公平锁
new ReentrantLock(true); // 公平锁
4. 可以查询锁状态
lock.getHoldCount(); // 重入次数
lock.isHeldByCurrentThread(); // 是否当前线程持有
5. 可以绑定多个条件变量
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
但是:
- synchronized 更简洁
- synchronized 自动释放(不需要 finally)
- synchronized 性能已经优化得很好
7.3 常见误区
❌ 误区1:可重入锁可以被不同线程重入
正确:只能被同一线程重入!
❌ 误区2:获取一次锁,可以多次释放
正确:获取几次,必须释放几次!
❌ 误区3:可重入锁不会死锁
正确:可重入锁避免了"自己锁自己"的死锁,
但仍然可能发生"循环等待"的死锁。
例如:
线程A持有锁1,等待锁2
线程B持有锁2,等待锁1
→ 死锁!(与可重入无关)
❌ 误区4:所有锁都是可重入的
正确:Java的 synchronized 和 ReentrantLock 是可重入的,
但你可以实现不可重入的锁。
总结
核心要点
1. 定义
同一线程可以多次获取同一把锁,不会被自己阻塞
2. 关键机制
- 记录持有线程(owner)
- 维护重入计数(holdCount)
- 匹配释放(获取N次,释放N次)
3. 解决问题
- 递归方法加锁
- 方法调用链加锁
- 避免自己锁住自己
4. Java 实现
- synchronized(天然可重入)
- ReentrantLock(显式可重入)
5. 生活类比
智能门锁:识别同一个人,记录进出次数
记忆口诀
可重入锁,同人可进
记录次数,匹配释放
递归调用,方法链用
避免自己锁自己的死锁
实战建议
1. 优先使用 synchronized
- 简洁
- 自动释放
- 性能足够
2. 需要高级功能时用 ReentrantLock
- 超时获取
- 可中断
- 公平锁
3. 一定要成对使用
lock() → unlock()
获取N次 → 释放N次
4. 使用 try-finally 保证释放
lock.lock();
try {
// 业务逻辑
} finally {
lock.unlock();
}
笔记创建时间:2025-12-11
适合:初学者、面试准备
我的收获:7 面试要点说的很好,然后 7.3 常见误区也分析的很好,还有 Q4 的 Reentrantlock 相比于 synchronized 的优势分析,实战建议也给的不错。学会了如何手撕 Reentrantlock。为什么没有手撕 synchronized。