可重入与可重入锁:多线程编程中的安全卫士
一、什么是可重入(Reentrant)?
1. 核心定义
可重入(Reentrant) 是指一段代码(如函数、方法或锁)在 同一线程 中被多次调用时,不会因重复访问而导致错误或死锁的特性。可重入性是线程安全的重要基础。
2. 可重入的常见场景
-
递归函数:函数直接或间接调用自身。
-
嵌套锁:同一线程多次获取同一把锁。
-
回调函数:在持有锁的情况下触发回调,回调中再次请求同一锁。
3. 示例:递归函数
public class Factorial {
// 可重入的递归函数
public synchronized int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // 递归调用自身
}
}
-
若锁不可重入,第二次调用
factorial(n-1)
时会被阻塞,导致死锁。
二、什么是可重入锁(Reentrant Lock)?
1. 核心定义
可重入锁 是一种允许同一线程多次获取同一把锁的同步机制。每次获取锁时,锁的计数器加1;释放锁时,计数器减1。只有当计数器归零时,锁才被完全释放。
2. 可重入锁的实现原理
-
持有线程标识:记录当前持有锁的线程。
-
计数器:统计同一线程的加锁次数。
-
公平性策略(可选):决定锁的获取顺序(公平或非公平)。
3. 可重入锁的典型实现
(1) Java 的 ReentrantLock
ReentrantLock lock = new ReentrantLock();
public void methodA() {
lock.lock();
try {
methodB(); // 嵌套调用
} finally {
lock.unlock();
}
}
public void methodB() {
lock.lock(); // 同一线程可重入
try {
// 业务逻辑
} finally {
lock.unlock();
}
}
(2) Java 的 synchronized
关键字
public synchronized void methodA() {
methodB(); // 嵌套调用
}
public synchronized void methodB() {
// 业务逻辑
}
三、可重入锁解决了什么问题?
1. 避免自死锁(Self-Deadlock)
-
问题场景:若锁不可重入,线程在持有锁的情况下再次请求该锁会被阻塞,导致死锁。
-
示例:
public class NonReentrantExample { public synchronized void methodA() { methodB(); // 若锁不可重入,此处会阻塞 } public synchronized void methodB() { // 业务逻辑 } }
-
可重入锁的解决:允许同一线程多次加锁,避免阻塞。
2. 支持递归调用
-
递归函数需要多次访问同一资源,可重入锁确保递归逻辑的正确性。
3. 简化嵌套锁设计
-
在复杂业务逻辑中,多个方法可能嵌套调用并共享同一锁,可重入锁简化了代码设计。
四、可重入锁 vs 不可重入锁
特性 | 可重入锁 | 不可重入锁 |
---|---|---|
同一线程多次加锁 | 允许(计数器累加) | 阻塞或死锁 |
实现复杂度 | 较高(需维护线程标识和计数器) | 较低 |
典型应用场景 | 递归、嵌套方法调用 | 简单临界区保护 |
性能 | 稍低(需额外状态管理) | 较高 |
五、可重入锁的底层实现(以 Java 为例)
1. ReentrantLock
的计数器实现
public class ReentrantLock implements Lock {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
// 记录持有锁的线程和计数器
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
}
}
-
计数器:通过
state
字段实现,每次加锁state++
,解锁state--
。 -
线程标识:通过
exclusiveOwnerThread
字段记录当前持有锁的线程。
2. synchronized
的锁升级机制
-
偏向锁:首次获取锁时标记线程ID。
-
轻量级锁:通过CAS竞争锁。
-
重量级锁:竞争激烈时升级为操作系统级互斥锁(Mutex)。
六、可重入锁的最佳实践
1. 始终在 finally
块中释放锁
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 业务逻辑
} finally {
lock.unlock(); // 确保锁被释放
}
2. 避免锁的过度嵌套
-
嵌套层级过多会增加锁的持有时间,降低并发性能。
3. 优先使用 ReentrantLock
的高级功能
-
可中断锁:
lock.lockInterruptibly()
-
超时获取锁:
lock.tryLock(long timeout, TimeUnit unit)
-
公平锁:
new ReentrantLock(true)
七、总结
1. 可重入锁的核心价值
-
解决同一线程多次加锁的死锁问题。
-
支持递归和嵌套调用,提升代码灵活性。
2. 适用场景
-
多线程环境下的复杂业务逻辑。
-
需要递归或嵌套访问共享资源的场景。
3. 选择建议
-
默认情况下优先使用
synchronized
(简洁高效)。 -
需要高级功能(如超时、公平性)时选择
ReentrantLock
。
通过合理使用可重入锁,开发者可以显著降低多线程编程中的死锁风险,构建更健壮的并发系统