什么是可重入锁?如何自己实现一个可重入锁?

#Java#面试八股

什么是可重入锁?如何自己实现一个可重入锁?

结论先行:可重入锁(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,唤醒等待者

两个实现细节的对照:

实现持有者记录在哪重入计数记在哪
synchronizedObjectMonitor 的 _owner 字段ObjectMonitor 的 _recursions 字段
ReentrantLockAQS 的 exclusiveOwnerThreadAQS 的 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 严格配对。

笔记加载中…