为什么使用可重入锁

在 Java 并发编程中,可重入锁(Reentrant Lock) 是一种核心的同步机制,其设计目的是为了解决多线程环境下资源竞争、死锁等问题,同时提升代码的灵活性和可维护性。以下是 Java 并发编程需要实现可重入锁的详细原因和原理:


1. 为什么需要可重入锁?

(1) 避免线程因重复请求锁导致的死锁

  • 问题场景
    如果一个线程在持有锁的情况下再次请求同一把锁,且锁不支持可重入性,线程会因为无法获取锁而阻塞,最终导致死锁
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Counter {
    private final Object lock = new Object();

    public void increment() {
    synchronized (lock) {
    // 递归调用
    recursiveIncrement();
    }
    }

    private void recursiveIncrement() {
    synchronized (lock) {
    // 如果锁不可重入,这里会死锁!
    }
    }
    }
    • 问题increment() 方法内部调用 recursiveIncrement(),而两者都使用 synchronized(lock),如果锁不可重入,线程会在 recursiveIncrement() 中尝试获取已经持有的锁时被阻塞,从而卡死。
    • 解决方案:可重入锁允许线程在持有锁的情况下多次获取同一把锁,避免死锁。

(2) 支持嵌套同步方法调用

  • 典型场景
    在继承关系中,子类的同步方法调用父类的同步方法时,若锁不可重入,会导致线程无法进入父类方法。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Parent {
    public synchronized void doSomething() {
    // ...
    }
    }

    class Child extends Parent {
    public synchronized void doSomething() {
    super.doSomething(); // 如果锁不可重入,会死锁!
    }
    }
    • 问题ChilddoSomething() 方法调用了 ParentdoSomething(),两者都使用内置锁(synchronized)。若锁不可重入,线程会因无法再次获取锁而卡死。
    • 解决方案:Java 的 synchronized 锁和 ReentrantLock 都是可重入的,允许这种嵌套调用。

(3) 提升代码的封装性和灵活性

  • 优势
    可重入锁允许开发者将同步逻辑封装在多个方法中,而无需担心线程因重复请求锁导致的死锁问题。这简化了并发代码的设计和维护。

2. 可重入锁的实现原理

Java 的可重入锁(如 ReentrantLocksynchronized)通过 计数器(State)和持有线程 实现可重入性:

  1. 计数器机制
    • 每个锁关联一个计数器(state)和一个持有线程。
    • 首次获取锁:计数器从 0 增加到 1,线程成为锁的持有者。
    • 重入获取锁:计数器递增(如 state++)。
    • 释放锁:计数器递减(如 state--),当计数器为 0 时,锁被完全释放,其他线程可以竞争。
  2. 线程检查机制
    • 当线程尝试获取锁时,会检查当前线程是否是锁的持有者。
    • 如果是:允许重入,并增加计数器。
    • 如果不是:阻塞线程,加入等待队列。

(1) ReentrantLock 的实现

  • 基于 AQS(AbstractQueuedSynchronizer)
    • ReentrantLock 使用 AQS 的 state 字段记录锁的重入次数。
    • 获取锁
      1
      2
      3
      4
      5
      6
      7
      if (当前线程是持有者) {
      state += 1;
      } else {
      CAS 尝试将 state 从 0 改为 1
      如果成功,线程成为持有者;
      否则,阻塞线程并加入等待队列。
      }
    • 释放锁
      1
      2
      3
      4
      state -= 1;
      if (state == 0) {
      唤醒等待队列中的线程;
      }

(2) synchronized 的实现

  • JVM 内置的可重入锁
    • 每个对象都有一个关联的监视器锁(Monitor Lock)。
    • 计数器:JVM 为每个锁维护一个计数器,记录当前线程持有锁的次数。
    • 示例
      1
      2
      3
      4
      5
      6
      7
      public synchronized void outerMethod() {
      innerMethod(); // 调用另一个 synchronized 方法
      }

      public synchronized void innerMethod() {
      // 可重入:同一线程可再次获取锁
      }

3. 可重入锁的优势

(1) 避免死锁

  • 允许线程在持有锁的情况下再次获取同一把锁,避免因重复请求锁导致的死锁。

(2) 支持递归调用

  • 适用于递归函数、嵌套同步方法等场景。

(3) 提高代码灵活性

  • 可重入锁(如 ReentrantLock)比 synchronized 更灵活,支持:
    • 公平锁/非公平锁:公平锁按等待顺序分配锁,避免线程饥饿。
    • 尝试获取锁:通过 tryLock() 方法尝试获取锁,避免无限阻塞。
    • 超时获取锁:通过 tryLock(timeout, unit) 设置超时时间。
    • 中断响应:通过 lockInterruptibly() 支持中断操作。

4. 可重入锁的应用场景

(1) 递归函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Counter {
private final ReentrantLock lock = new ReentrantLock();

public void increment(int depth) {
lock.lock();
try {
count++;
if (depth > 0) {
increment(depth - 1); // 递归调用
}
} finally {
lock.unlock();
}
}
}
  • 关键:线程在递归调用中多次获取同一把锁,不会被阻塞。

(2) 父类与子类的同步方法调用

1
2
3
4
5
6
7
8
9
10
11
class Parent {
public synchronized void doSomething() {
// ...
}
}

class Child extends Parent {
public synchronized void doSomething() {
super.doSomething(); // 可重入:同一线程可再次获取锁
}
}

(3) 复杂的业务逻辑

  • 在业务逻辑中,多个方法可能需要共享同一把锁,而无需担心死锁问题。

5. 注意事项

(1) 可重入锁不能完全避免死锁

  • 死锁的根本原因是循环依赖,例如多个线程互相持有对方需要的锁。可重入锁只能避免同一线程因重复请求锁导致的死锁,但无法解决多线程间的死锁
    1
    2
    Thread1: lockA -> lockB  
    Thread2: lockB -> lockA
    • 解决方案:遵循统一的锁获取顺序,或使用工具分析死锁。

(2) 避免过度依赖可重入性

  • 过度依赖可重入性可能导致代码逻辑复杂,建议在必要时使用。

(3) 性能权衡

  • 公平锁:性能较低,但避免线程饥饿。
  • 非公平锁:性能较高,但可能导致某些线程长时间等待。

6. 总结

特性 可重入锁
核心目的 避免线程因重复请求锁导致的死锁,支持嵌套同步方法调用
实现机制 计数器(State) + 持有线程检查
典型实现 ReentrantLock(显式锁)、synchronized(内置锁)
优势 避免死锁、支持递归/嵌套调用、灵活控制锁行为
适用场景 递归函数、父子类同步方法、复杂的业务逻辑
注意事项 不能完全避免死锁、需合理设计锁的获取顺序、性能权衡

通过可重入锁,Java 并发编程能够在复杂场景下保持代码的简洁性和安全性,是多线程开发中不可或缺的工具。


为什么使用可重入锁
http://example.com/2025/07/05/为什么使用可重入锁/
作者
無鎏雲
发布于
2025年7月5日
许可协议