什么是可重入锁?如何自己实现一个可重入锁?
结论先行:可重入锁(reentrant lock)指同一个线程可以重复获取同一把锁而不会把自己锁死的锁。实现只需要两样东西:记录"当前持有者是谁"和"重入了多少次"。synchronized 和 ReentrantLock 都是可重入的。
1. 为什么必须可重入
public synchronized void a() {
b(); // 同一线程再次进入 synchronized 方法
}
public synchronized void b() {
// 如果锁不可重入,这里会永远等自己释放锁 → 死锁
}
真实代码里这种调用极常见:事务方法调用另一个事务方法、模板方法回调子类、递归加锁。没有可重入,几乎无法写出正常的同步代码。
2. 关键状态:持有者 + 重入计数
线程 A 调用 lock() state: 0 → 1 owner: null → A
线程 A 再次 lock() state: 1 → 2 owner: A
线程 A 再次 lock() state: 2 → 3
线程 A unlock() state: 3 → 2
线程 A unlock() state: 2 → 1
线程 A unlock() state: 1 → 0 owner: A → null,唤醒等待者
两个实现细节的对照:
| 实现 | 持有者记录在哪 | 重入计数记在哪 |
|---|---|---|
synchronized | ObjectMonitor 的 _owner 字段 | ObjectMonitor 的 _recursions 字段 |
ReentrantLock | AQS 的 exclusiveOwnerThread | AQS 的 state(int) |
unlock() 必须与 lock() 成对出现:少调一次,锁永远不释放;多调一次,抛 IllegalMonitorStateException(synchronized 则是 monitor 计数错乱,行为更隐蔽)。
3. 手写一个可重入锁
public class SimpleReentrantLock {
private Thread owner; // 当前持有锁的线程
private int holdCount; // 重入次数
public synchronized void lock() {
Thread cur = Thread.currentThread();
while (owner != null && owner != cur) { // 被别人占着,等
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
owner = cur;
holdCount++; // 自己再次进来 → 计数 +1
}
public synchronized void unlock() {
if (owner != Thread.currentThread()) {
throw new IllegalMonitorStateException("当前线程未持有该锁");
}
if (--holdCount == 0) { // 计数归零才算真正释放
owner = null;
notifyAll(); // 唤醒一个等待者
}
}
}
验证一下重入:
SimpleReentrantLock lock = new SimpleReentrantLock();
lock.lock();
try {
lock.lock(); // 同一线程重入,holdCount = 2,不会死锁
try {
System.out.println("内层临界区");
} finally {
lock.unlock(); // 回到 1
}
} finally {
lock.unlock(); // 回到 0,真正释放
}
把 while (owner != null && owner != cur) 改成 while (owner != null),它立刻退化成不可重入锁,上面这段代码会卡死在第二次 lock()。
4. 可重入的代价
| 收益 | 代价 |
|---|---|
| 递归/嵌套调用不需要拆锁 | 每次加锁都要判断持有者,多一次比较 |
| 代码可组合(子方法也能加锁) | 释放必须严格配对,漏 unlock 会永久占锁 |
| 语义直观,符合直觉 | 重入次数是 int,理论上可溢出(实际上不可能达到 21 亿层) |
5. 常见追问
追问一:synchronized 的重入是怎么实现的?
靠对象头指向的 ObjectMonitor:线程进入时检查 _owner 是否是自己,是则 _recursions++;退出时 --,减到 0 才真正释放并唤醒 _EntryList 里的线程。所以"锁升级"和"可重入"是两个独立机制,不要混为一谈。
追问二:StampedLock 为什么不可重入?
它用 stamp(版本戳)表示一次加锁,设计目标是极致读性能,放弃了持有者追踪。重复获取同一把写锁会直接死锁——这是它"更快但更难用"的直接代价。
追问三:重入锁一定安全吗? 不一定。可重入只是让"自己等自己"不成立,跨线程的循环等待照样死锁。两个线程分别持有 A、B 再去申请对方的锁,可重入救不了。
追问四:ReentrantLock 的 state 能超过 1 吗?
能,state 就是重入次数。getHoldCount() 返回的就是它,调试重入泄漏时非常有用。
小结
可重入 = "锁认人":持有者是当前线程就直接放行并计数,释放时计数归零才真正交还。实现只要一个 owner 字段加一个计数器,难点不在原理,而在保证 lock/unlock 严格配对。