在黑马点评项目实战中,提到了可重入锁,然后我想到了是不是不同业务在同一线程内反复获取同一把锁。本文来讨论一下为什么锁需要可重入。
一、可重入锁的核心:“同一线程多次获取同一把锁”
可重入(Reentrant) 的字面意思是“允许重新进入”,在锁的上下文中,特指同一线程可以多次获取同一把锁而不会被阻塞。这与“不同业务”无直接关联,而是针对同一线程内的嵌套调用或递归调用场景设计的。
1. 可重入的典型场景:同一线程的嵌套调用
假设你有一个递归方法或嵌套调用的业务逻辑,例如:
public void methodA() {lock.lock(); // 第一次加锁try {// 业务逻辑1...methodB(); // 调用methodB} finally {lock.unlock(); // 最终释放锁(需确保只释放一次)}
}public void methodB() {lock.lock(); // 嵌套调用,需要再次加锁try {// 业务逻辑2...} finally {lock.unlock();}
}
如果锁是不可重入的,当methodA
获取锁后调用methodB
时,methodB
尝试再次加锁会被阻塞(因为锁已被当前线程持有),导致死锁。而可重入锁允许同一线程多次加锁(每次加锁重入次数+1),直到所有加锁操作都被释放(重入次数减至0),从而避免死锁。
2. 与“不同业务”的区别
这里的“不同业务”并非指不同线程或不同功能模块,而是同一线程内的不同代码路径(如递归、循环调用)。例如:
- 电商下单场景中,主线程先校验库存(调用
checkStock()
),再扣减库存(调用deductStock()
),两个方法都需要同一把锁。若锁不可重入,checkStock()
加锁后,deductStock()
会因无法获取锁而阻塞;可重入锁则允许checkStock()
加锁后,deductStock()
直接获取已持有的锁(重入次数+1)。
二、为什么需要可重入锁?应对复杂业务的“嵌套锁需求”
可重入锁的设计主要是为了简化复杂业务逻辑中的锁管理。在真实业务中,嵌套调用(如方法A调用方法B,两者都需要同一把锁)非常常见,若使用不可重入锁,开发者需手动维护锁的获取次数(例如,通过计数器记录嵌套层级),否则容易因忘记释放锁或重复释放导致死锁或数据不一致。
1. 不可重入锁的痛点
假设使用不可重入锁,开发者需手动处理嵌套调用的锁逻辑:
public class NonReentrantLockExample {private int lockCount = 0; // 手动维护重入次数private final Object lock = new Object();public void methodA() {synchronized (lock) { // 不可重入锁,第一次加锁lockCount++;try {// 业务逻辑...methodB(); // 调用methodB} finally {lockCount--;if (lockCount == 0) {// 手动释放锁(仅当重入次数为0时)}}}}public void methodB() {synchronized (lock) { // 不可重入锁,此处会阻塞!// 业务逻辑...}}
}
这种情况下,methodB
的synchronized
块会因无法获取锁而永久阻塞,必须通过复杂的计数器逻辑手动管理锁的释放,容易出错。
2. 可重入锁的简化
Redisson的可重入锁通过自动维护重入次数解决了这一问题:
- 当同一线程首次获取锁时,重入次数初始化为1;
- 若同一线程再次获取同一把锁(如嵌套调用),重入次数递增(如2、3...);
- 释放锁时,重入次数递减,直到次数为0时才真正释放锁(通知Redis删除锁)。
开发者无需手动维护重入次数,锁的获取和释放逻辑与普通单次加锁一致,大大降低了复杂度。
三、锁值(重入次数)的含义:“当前线程持有锁的次数”
Redisson的可重入锁在Redis中存储的键值对结构大致如下(通过RedissonLock
类的tryLock
方法实现的Lua脚本):
-- 锁的键:lockKey(如"lock:user:123")
-- 锁的值:持有者ID(如线程ID+客户端ID) + 重入次数(初始为1)
if not redis.call('exists', KEYS[1]) thenredis.call('hset', KEYS[1], ARGV[2], 1) -- 持有者ID -> 重入次数1redis.call('pexpire', KEYS[1], ARGV[1]) -- 设置超时时间return 1
elseif redis.call('hexists', KEYS[1], ARGV[2]) == 1 thenlocal count = redis.call('hincrby', KEYS[1], ARGV[2], 1) -- 重入次数+1redis.call('pexpire', KEYS[1], ARGV[1]) -- 重置超时时间(防止过期)return nil
elsereturn 0
end
其中,锁的值(存储在Redis的Hash结构中) 包含两部分:
- 持有者ID:唯一标识当前持有锁的线程(如
thread:123@client:456
,避免不同节点的线程ID冲突); - 重入次数:当前线程已获取该锁的次数(初始为1,每次重入+1,释放时-1)。
1. 锁值≠0的含义
当锁值(重入次数)大于0时,说明当前线程仍持有该锁(可能是在嵌套调用中,或尚未释放所有重入次数)。此时,其他线程尝试获取该锁会被阻塞(直到锁超时或当前线程释放)。
2. 锁值=0的含义
当锁值等于0时,说明当前线程已完全释放该锁(所有重入次数已耗尽),Redis会删除该锁的键,其他线程可以重新竞争获取。
四、总结:可重入锁的本质是“同一线程的锁重用”
Redisson可重入锁的“可重入”核心是允许同一线程多次获取同一把锁,解决的是复杂业务中嵌套调用(如递归、方法调用链)导致的锁冲突问题。其本质是“同一线程的锁重用”,而非“不同业务的锁共享”。
锁值(重入次数)记录了当前线程持有锁的次数,只有当次数减至0时,锁才真正释放。这一设计避免了开发者手动维护嵌套锁的复杂性,是分布式系统中处理复杂业务逻辑的重要工具。