一. 核心定义
锁的可重入性 (Reentrancy)是指同一线程在外层方法已获取锁的前提下,能够在内层方法中自动再次获取该锁的能力。这种机制允许线程在持有锁的情况下,递归调用同步方法或嵌套进入其他需要同一锁的代码块,而不会因锁未被释放导致自身阻塞或死锁。
二. 技术原理
- 实现机制
- 计数器与线程绑定:每个可重入锁维护两个核心属性
- 持有线程(Owner Thread):记录当前锁的持有者
- 重入计数器(Recursion Counter):记录锁被同一线程重复获取的次数。初始值为0(未锁定),线程首次获取锁时计数器+1,每次重入+1;释放时每次-1,归零时完全释放锁。
- 内存语义:与synchronized同步块具有相同的内存可见性保证,确保锁释放前的修改对所有后续获取该锁的线程可见。
- 典型实现对比
实现方式 | 技术细节 | 示例场景 |
---|---|---|
synchronized | JVM隐式管理计数器(通过对象头的MarkWord) 无需手动释放锁 | 递归方法调用 |
ReentrantLock | 基于AQS框架的state变量实现 通过getHoldCount()查询当前持有次数 | 需要锁中断/超时的复杂同步 |
三. 必要性分析
- 避免自死锁
不可重入锁的缺陷: 若锁不可重入,当线程在外层方法获取锁后,再调用内层需同一锁的方法时,会因锁未被释放而永久阻塞自身(形成死锁)。
java
// 不可重入锁的典型死锁场景
public void outer() {
lock.lock();
try {
inner(); //调用内层方法
} finally {
lock.unlock();
}
}
public void inner() {
lcok.lock(); // 此处将阻塞,因外层未被释放
try{
//省略
} finally{
lock.unlock();
}
}
- 简化嵌套调用
在分层架构或递归算法中,可重入性允许开发者无需关心锁的层级关系。比如:
java
//递归场景中的可重入锁应用
public synchronized void reduceStock(int n) {
if(n <= 0){
return;
}
reduceStock(n - 1); //自动重入锁
}
四. Java实现细节
1. synchronized的底层实现
对象头结构:每个Java对象头包含"Mark Word"字段,其中:
- 锁标志位(2-3位)标识锁的状态(偏向锁/轻量级锁/重量级锁)
- 偏向线程ID(若为偏向锁)
计数器管理:
- 每次进入同步块时,若线程已经持有锁,JVM通过CAS操作递增锁计数器,而非触发真正的锁竞争。
2. ReentrantLock的显示控制
- AQS框架实现:
java
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // 获取当前锁状态
if (c == 0) { // 无锁状态
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 重入分支
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
- 公平性策略: 支持公平锁(按请求顺序分配)与非公平锁(先尝试插队,插队失败再排队),通过构造器参数fair指定。
五. 应用场景与最佳实践
1. 典型使用场景
场景类型 | 案例说明 | 技术优势 |
---|---|---|
递归算法 | 树形结构遍历、动态规划等需要重复进入同步域的算法 | 避免递归层数导致的死锁 |
分层锁设计 | 服务调用DAO层时,两级方法需共享同一事物锁 | 简化跨层同步逻辑 |
复杂事务 | 包含多个自操作的原子操作任务(如库存扣减+订单创建) | 保证多步骤操作的原子性 |
2. 使用规范
-
释放锁对称性
- ReentrantLock需要严格遵循lock()与unlock()配对,通常使用try-finally块确保释放。
javaReentrantLock lock = new ReentrantLock(); public void safeMethod(){ lock.lock(); try { // 临界区代码 nestedMethod(); // 嵌套方法(可重如调用) }finally { // 确保释放 lock.unlock(); } }
-
避免过度重入:尽管可重入性提供便利,但深层嵌套可能导致计数器溢出(如Integer.MAX_VALUE)或调试困难。
六. 扩展:分布式环境下的可重入性
在分布式锁场景中(如Redis集群),可重入性通过以下方式实现:
1.线程唯一标识:
使用ThreadId+JVM实例ID作为锁持有者标识,存入Redis Hash结构。
2.重入计数存储:
csharp
HSET lock:order_123 owner "jvm1-thread-5" count 2
每次重入时递增count字段,释放时递减,归零后删除key。
七. 对比
维度 | 可重入锁 | 不可重入锁 |
---|---|---|
死锁风险 | 避免同一线程的自我阻塞 | 容易导致嵌套调用死锁 |
实现复杂度 | 需维护计数器与线程绑定关系 | 仅需布尔状态标识 |
适用场景 | 递归、分层调用、复杂事务 | 简单无嵌套的单层同步 |
性能开销 | 略高(计数操作) | 较低 |
参考资料
- 《Java并发编程实战-机械工业出版社》