什么是可重入锁,如何实现可重入锁?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(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+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 概念理解:面试官不仅仅是想知道 "同一个线程可以重复获取同一把锁",更是想知道你是否理解 "为什么必须可重入"——如果锁不可重入,一个同步方法调用另一个同步方法会怎样?直接死锁。

  2. 实现原理:考察你是否清楚 synchronized 通过 Monitor 计数器实现重入,ReentrantLock 通过 AQS 的 state + exclusiveOwnerThread 实现重入。这两种实现方式的异同。

  3. AQS 源码级理解:这是加分项。能说出 nonfairTryAcquire() 中判断 current == getExclusiveOwnerThread() 的逻辑,说明你真的看过源码。

核心答案

先甩结论:可重入锁指的是同一个线程可以重复获取同一把锁而不会被阻塞。每次获取锁计数器 +1,每次释放计数器 -1,减到 0 才真正释放锁。Java 中 synchronizedReentrantLock 都是可重入锁。

如果锁不可重入,一个同步方法调用另一个同步方法时,线程会永远等自己释放锁——也就是 自己锁死自己,经典的自锁死锁。

深度解析

一、为什么必须可重入?

先看一个日常开发中非常常见的场景:

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)
公平性 非公平 支持公平/非公平

面试高频追问

  1. 可重入锁的最大重入次数是多少?

    ReentrantLockstateint 类型,理论上最大 2^31 - 1(约 21 亿次)。synchronized_count 没有明确上限。正常业务中重入超过几十次都是代码设计有问题,不用担心溢出。

  2. 如果 lock()unlock() 不匹配会怎样?

    少调一次 unlock():锁永远不会释放,其他线程永远等待。多调一次 unlock()tryRelease() 会抛 IllegalMonitorStateException,因为 state 已经是 0 了,当前线程又不是持有者。

  3. ReentrantReadWriteLock 的读锁也是可重入的吗?

    是的。读锁和写锁都支持重入。写锁重入时 state 的低 16 位 +1,读锁重入时每个线程有自己的计数器(ThreadLocal 记录),高 16 位记录总读锁数。

常见面试变体

  • "synchronized 是可重入锁吗?"
  • "ReentrantLock 的可重入是怎么实现的?"
  • "为什么锁必须可重入?"
  • "ReentrantReadWriteLock 的读写锁如何实现重入?"

记忆口诀

可重入:同一个线程可以重复加锁,计数器 +1/-1,减到 0 才释放

实现核心:两个东西——持有者(谁拿着锁)+ 计数器(重入了几次)

源码关键current == getExclusiveOwnerThread() → 是持有者就 state++

总结

可重入锁的核心是 "同一个线程可以重复获取同一把锁",通过 持有者标识 + 计数器 实现。synchronizedMonitor_owner + _countReentrantLock 用 AQS 的 exclusiveOwnerThread + state。面试时把 "为什么需要可重入"(避免自锁死锁)和 "怎么实现"(判断持有者 + 计数器递增递减)讲清楚,再配上 nonfairTryAcquire() 的源码解读,这道题就是高分。