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

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

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 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/

面试考察点

面试官问这个问题,核心是想考察你对以下方面的理解深度:

  1. 对锁基本概念的理解:你是否清晰理解 “可重入” 这个核心特性及其解决的问题。
  2. 对并发编程实践的掌握:可重入锁是避免死锁、编写健壮并发代码的关键工具之一。面试官想知道你是否理解其在实际编码中的应用场景和重要性。
  3. 对底层实现原理的探究能力:不仅仅是知道 “是什么”,更想知道你能否阐述其实现机制,这能体现你是否真正“吃透”了这个概念。
  4. 对 Java 并发体系的知识广度:能否联系 synchronized 关键字和 ReentrantLock 类,说明 Java 中两种主要的可重入锁实现。

核心答案

可重入锁(Reentrant Lock)是一种特殊的线程同步机制,它允许同一个线程在已经持有该锁的情况下,可以多次成功获取同一把锁,而不会因为等待自己释放锁而导致死锁。

在 Java 中,synchronized 关键字java.util.concurrent.locks.ReentrantLock都是典型的可重入锁实现。

深度解析

原理/机制

可重入性的实现,核心在于为锁关联一个 持有线程标识 和一个 重入计数器

  1. 初次加锁:当线程第一次请求锁时,JVM 或 ReentrantLock 会记录当前持有锁的线程,并将计数器设置为 1。
  2. 重入加锁:同一个线程再次请求该锁时,系统会检查请求线程是否为当前持有线程。如果是,则直接将计数器加 1,并立即成功 “获取” 锁(实际上并未阻塞)。
  3. 释放锁:线程每次调用 unlock() 或退出 synchronized 块时,计数器减 1
  4. 完全释放:当计数器减到 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 环境 | 极少数特殊场景,或用于教学理解锁的基本概念 |

  • 常见误区

    1. 误区一:可重入等于高并发/高性能。 错。可重入性解决的是正确性问题(避免自死锁),它本身不直接提升并发性能。
    2. 误区二:synchronized 不是可重入锁。 错。这是最容易混淆的点。从 JDK 1.0 开始,synchronized 实现的监视器锁就是可重入的
    3. 混淆“重入”与“并发”:可重入指的是 “同一个线程” 的重复进入,而非“多个线程”的并发访问。多个线程的并发安全仍需由锁的互斥性来保证。

最佳实践

  1. 优先使用内置机制:对于大多数同步场景,优先考虑使用 synchronized。JVM 会对其进行持续优化(如锁升级),且代码更简洁,不易出错(自动释放)。
  2. 按需选择 ReentrantLock:当需要尝试非阻塞获取锁(tryLock)、可中断的锁等待、公平锁策略、或绑定多个条件(Condition 等高级功能时,再使用 ReentrantLock
  3. 确保释放:使用 ReentrantLock 时,必须finally 块中调用 unlock() 以确保锁在任何情况下都能被释放,防止死锁。
  4. 理解重入深度:虽然重入避免了死锁,但过深的递归重入(如 Bug 导致的无限递归)最终会引发 StackOverflowError,而非死锁。

总结

可重入锁通过持有线程标识 + 重入计数器的机制,允许同一线程多次获取同一把锁,从根本上防止了线程自身导致的死锁,是 Java 并发编程中保证同步代码块或方法正确、灵活调用的基石。