可重入锁详解-ReentrantLock和Synchronzied

可重入锁详解(从零理解)

目录

  1. 什么是可重入锁
  2. 为什么需要可重入锁
  3. 不可重入锁的问题
  4. 可重入锁的实现原理
  5. 代码示例
  6. 常见场景
  7. 面试要点

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。

相关推荐
K哥11259 个月前
【多线程】线程不安全问题
java·volatile·可重入锁·线程锁·线程安全问题·wait和notify
GGBondlctrl1 年前
【JavaEE初阶】深入解析死锁的产生和避免以及内存不可见问题
java·开发语言·死锁·内存可见性·哲学家就餐问题·可重入锁