为什么使用可重入锁
在 Java 并发编程中,可重入锁(Reentrant Lock) 是一种核心的同步机制,其设计目的是为了解决多线程环境下资源竞争、死锁等问题,同时提升代码的灵活性和可维护性。以下是 Java 并发编程需要实现可重入锁的详细原因和原理:
1. 为什么需要可重入锁?
(1) 避免线程因重复请求锁导致的死锁
- 问题场景:
如果一个线程在持有锁的情况下再次请求同一把锁,且锁不支持可重入性,线程会因为无法获取锁而阻塞,最终导致死锁。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class 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
11class Parent {
public synchronized void doSomething() {
// ...
}
}
class Child extends Parent {
public synchronized void doSomething() {
super.doSomething(); // 如果锁不可重入,会死锁!
}
}- 问题:
Child
的doSomething()
方法调用了Parent
的doSomething()
,两者都使用内置锁(synchronized
)。若锁不可重入,线程会因无法再次获取锁而卡死。 - 解决方案:Java 的
synchronized
锁和ReentrantLock
都是可重入的,允许这种嵌套调用。
- 问题:
(3) 提升代码的封装性和灵活性
- 优势:
可重入锁允许开发者将同步逻辑封装在多个方法中,而无需担心线程因重复请求锁导致的死锁问题。这简化了并发代码的设计和维护。
2. 可重入锁的实现原理
Java 的可重入锁(如 ReentrantLock
和 synchronized
)通过 计数器(State)和持有线程 实现可重入性:
- 计数器机制:
- 每个锁关联一个计数器(
state
)和一个持有线程。 - 首次获取锁:计数器从
0
增加到1
,线程成为锁的持有者。 - 重入获取锁:计数器递增(如
state++
)。 - 释放锁:计数器递减(如
state--
),当计数器为0
时,锁被完全释放,其他线程可以竞争。
- 每个锁关联一个计数器(
- 线程检查机制:
- 当线程尝试获取锁时,会检查当前线程是否是锁的持有者。
- 如果是:允许重入,并增加计数器。
- 如果不是:阻塞线程,加入等待队列。
(1) ReentrantLock
的实现
- 基于 AQS(AbstractQueuedSynchronizer):
ReentrantLock
使用 AQS 的state
字段记录锁的重入次数。- 获取锁:
1
2
3
4
5
6
7if (当前线程是持有者) {
state += 1;
} else {
CAS 尝试将 state 从 0 改为 1;
如果成功,线程成为持有者;
否则,阻塞线程并加入等待队列。
} - 释放锁:
1
2
3
4state -= 1;
if (state == 0) {
唤醒等待队列中的线程;
}
(2) synchronized
的实现
- JVM 内置的可重入锁:
- 每个对象都有一个关联的监视器锁(Monitor Lock)。
- 计数器:JVM 为每个锁维护一个计数器,记录当前线程持有锁的次数。
- 示例:
1
2
3
4
5
6
7public synchronized void outerMethod() {
innerMethod(); // 调用另一个 synchronized 方法
}
public synchronized void innerMethod() {
// 可重入:同一线程可再次获取锁
}
3. 可重入锁的优势
(1) 避免死锁
- 允许线程在持有锁的情况下再次获取同一把锁,避免因重复请求锁导致的死锁。
(2) 支持递归调用
- 适用于递归函数、嵌套同步方法等场景。
(3) 提高代码灵活性
- 可重入锁(如
ReentrantLock
)比synchronized
更灵活,支持:- 公平锁/非公平锁:公平锁按等待顺序分配锁,避免线程饥饿。
- 尝试获取锁:通过
tryLock()
方法尝试获取锁,避免无限阻塞。 - 超时获取锁:通过
tryLock(timeout, unit)
设置超时时间。 - 中断响应:通过
lockInterruptibly()
支持中断操作。
4. 可重入锁的应用场景
(1) 递归函数
1 |
|
- 关键:线程在递归调用中多次获取同一把锁,不会被阻塞。
(2) 父类与子类的同步方法调用
1 |
|
(3) 复杂的业务逻辑
- 在业务逻辑中,多个方法可能需要共享同一把锁,而无需担心死锁问题。
5. 注意事项
(1) 可重入锁不能完全避免死锁
- 死锁的根本原因是循环依赖,例如多个线程互相持有对方需要的锁。可重入锁只能避免同一线程因重复请求锁导致的死锁,但无法解决多线程间的死锁。
1
2Thread1: lockA -> lockB
Thread2: lockB -> lockA- 解决方案:遵循统一的锁获取顺序,或使用工具分析死锁。
(2) 避免过度依赖可重入性
- 过度依赖可重入性可能导致代码逻辑复杂,建议在必要时使用。
(3) 性能权衡
- 公平锁:性能较低,但避免线程饥饿。
- 非公平锁:性能较高,但可能导致某些线程长时间等待。
6. 总结
特性 | 可重入锁 |
---|---|
核心目的 | 避免线程因重复请求锁导致的死锁,支持嵌套同步方法调用 |
实现机制 | 计数器(State) + 持有线程检查 |
典型实现 | ReentrantLock (显式锁)、synchronized (内置锁) |
优势 | 避免死锁、支持递归/嵌套调用、灵活控制锁行为 |
适用场景 | 递归函数、父子类同步方法、复杂的业务逻辑 |
注意事项 | 不能完全避免死锁、需合理设计锁的获取顺序、性能权衡 |
通过可重入锁,Java 并发编程能够在复杂场景下保持代码的简洁性和安全性,是多线程开发中不可或缺的工具。
为什么使用可重入锁
http://example.com/2025/07/05/为什么使用可重入锁/