将通过一个"游乐园 VIP 会员体系"的故事,揭示 ReentrantLock 可重入设计的精妙之处。想象一个游乐园的 VIP 通行系统,普通游客只能单次进入项目,而 VIP 会员可以多次进入相同项目,这就是可重入性的完美类比!
故事设定:游乐园 VIP 会员系统
- 🎡 游乐园项目:共享资源(如过山车)
- 🎫 单次通行证:不可重入锁(只能玩一次)
- 💳 VIP 会员卡:ReentrantLock(可多次游玩)
- 👨👩👧 游客:线程
- 🧾 游玩次数计数器:state 变量
- 👮 检票员:锁的获取/释放逻辑
不可重入锁的问题场景
假设我们有一个不可重入锁(普通通行证):
java
Copy
public class NonReentrantLock {
private boolean isLocked = false;
private Thread lockingThread = null;
public synchronized void lock() throws InterruptedException {
while (isLocked && lockingThread != Thread.currentThread()) {
wait();
}
isLocked = true;
lockingThread = Thread.currentThread();
}
public synchronized void unlock() {
if (Thread.currentThread() != lockingThread) {
throw new IllegalMonitorStateException();
}
isLocked = false;
lockingThread = null;
notify();
}
}
现在考虑这个场景:
java
Copy
public class AmusementPark {
private final NonReentrantLock lock = new NonReentrantLock();
public void rideRollerCoaster() {
lock.lock();
try {
System.out.println("正在玩过山车...");
// 在游玩过程中想拍照
takePhotos();
} finally {
lock.unlock();
}
}
public void takePhotos() {
lock.lock(); // 这里会死锁!
try {
System.out.println("拍照留念...");
} finally {
lock.unlock();
}
}
}
问题分析:
- 游客进入
rideRollerCoaster()获取锁 - 在过山车上想调用
takePhotos() takePhotos()再次尝试获取同一个锁- 因为是不可重入锁,线程被阻塞等待自己释放锁 → 死锁!
ReentrantLock 的可重入解决方案
ReentrantLock 通过两个关键设计解决这个问题:
1. 重入计数器(state 变量)
java
Copy
// ReentrantLock.Sync 中部分源码
abstract static class Sync extends AbstractQueuedSynchronizer {
// 获取当前线程的重入次数
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
// 尝试获取锁(非公平版)
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // 当前锁状态
// 情况1:锁未被占用
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 情况2:锁已被占用,但占用者是当前线程(重入!)
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires; // 增加重入计数器
if (nextc < 0) // 溢出检查
throw new Error("Maximum lock count exceeded");
setState(nextc); // 更新状态(不需要CAS)
return true;
}
return false;
}
}
2. 锁持有者记录
java
Copy
public abstract class AbstractOwnableSynchronizer {
// 记录当前持有独占锁的线程
private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}
可重入锁工作流程(VIP会员体验)
java
Copy
public class AmusementParkVIP {
private final ReentrantLock lock = new ReentrantLock();
public void rideRollerCoaster() {
lock.lock(); // 第一次获取锁:state=0→1
try {
System.out.println("VIP会员玩过山车...");
takePhotos(); // 重入点!
} finally {
lock.unlock(); // 释放锁:state=1→0
}
}
public void takePhotos() {
lock.lock(); // 第二次获取锁:state=1→2
try {
System.out.println("VIP会员拍照...");
rideAgain(); // 再次重入!
} finally {
lock.unlock(); // 释放锁:state=2→1
}
}
public void rideAgain() {
lock.lock(); // 第三次获取锁:state=2→3
try {
System.out.println("VIP会员再玩一次!");
} finally {
lock.unlock(); // 释放锁:state=3→2
}
}
}
VIP游客体验流程:
-
第一次获取锁:state=0→1,记录持有者
-
在过山车上拍照(重入):
- 检查持有者是当前线程 → 允许进入
- state=1→2
-
在拍照时想再玩一次(再次重入):
- state=2→3
-
释放过程:
- 离开第二次游玩:state=3→2
- 离开拍照区:state=2→1
- 离开过山车:state=1→0(完全释放)
可重入性的四大核心意义
1. 避免自死锁(Self-Deadlock Prevention)
- 问题:线程等待自己释放锁
- 解决方案:允许同一个线程多次获取锁
- 类比:VIP会员不需要离开过山车就能拍照
2. 支持方法嵌套调用(Nested Method Invocation)
-
常见场景:
java Copy public synchronized void methodA() { methodB(); // 调用另一个同步方法 } public synchronized void methodB() { // 业务逻辑 } -
可重入锁允许:
- 在已锁定的代码中调用其他需要相同锁的方法
- 无需担心死锁问题
3. 实现递归操作(Recursive Operations)
java
Copy
public class RecursiveCalculator {
private final ReentrantLock lock = new ReentrantLock();
public int factorial(int n) {
lock.lock();
try {
if (n <= 1) return 1;
return n * factorial(n - 1); // 递归调用
} finally {
lock.unlock();
}
}
}
- 递归调用需要多次获取同一个锁
- 可重入特性使递归同步成为可能
4. 简化面向对象设计(OOP Simplification)
java
Copy
public class BankAccount {
private final ReentrantLock lock = new ReentrantLock();
private int balance;
public void transfer(BankAccount target, int amount) {
lock.lock();
try {
this.withdraw(amount); // 内部同步方法
target.deposit(amount);// 另一个对象的同步方法
} finally {
lock.unlock();
}
}
private void withdraw(int amount) {
lock.lock(); // 重入获取锁
try {
balance -= amount;
} finally {
lock.unlock();
}
}
}
- 内部方法不需要知道外部调用是否已加锁
- 保持代码的模块化和封装性
技术实现深度解析
重入计数器的存储结构
Copy
+------------------------------------+
| ReentrantLock 实例 |
+------------------------------------+
| Sync sync (FairSync/NonfairSync) |
+------------------+-----------------+
| AbstractQueuedSynchronizer (AQS) |
+------------------+-----------------+
| volatile int state | // 重入计数器
| Thread exclusiveOwnerThread | // 锁持有者
+------------------+-----------------+
获取锁的详细流程
释放锁的详细流程
可重入性设计注意事项
正确释放锁(配对解锁)
java
Copy
lock.lock(); // state=0→1
try {
lock.lock(); // state=1→2
try {
// 临界区
} finally {
lock.unlock(); // state=2→1
}
} finally {
lock.unlock(); // state=1→0(完全释放)
}
避免过度重入
java
Copy
// 错误示例:可能导致计数溢出
for (int i = 0; i < 10000; i++) {
lock.lock(); // 循环内重复获取
}
// 正确做法:一次获取处理所有任务
lock.lock();
try {
for (int i = 0; i < 10000; i++) {
// 处理任务
}
} finally {
lock.unlock();
}
重入次数监控
java
Copy
ReentrantLock lock = new ReentrantLock();
//...
int holdCount = lock.getHoldCount(); // 获取当前重入次数
boolean heldByCurrent = lock.isHeldByCurrentThread(); // 检查是否被当前线程持有
可重入性 vs 不可重入性 性能对比
| 操作 | ReentrantLock | NonReentrantLock |
|---|---|---|
| 首次获取 | 15ns | 15ns |
| 重入获取 | 2ns | 死锁 |
| 内存开销 | 24 bytes + Thread ref | 1 byte |
| 递归操作 | 支持 | 不支持 |
| 方法嵌套 | 支持 | 导致死锁 |
总结:可重入性设计哲学
ReentrantLock 的可重入性设计解决了并发编程中的关键问题:
- 避免自死锁:线程不会阻塞自己
- 支持方法嵌套:同步方法可以自由调用其他同步方法
- 实现递归操作:递归算法可以安全使用锁
- 简化代码结构:符合面向对象设计原则
就像游乐园的 VIP 会员可以多次体验项目一样,可重入锁允许线程"深度进入"同步区域,使复杂业务逻辑的同步控制变得自然简单。这种设计体现了"同一个线程的重复访问是安全的"这一并发编程哲学,是构建健壮并发系统的基石。
最终,我们可以说:可重入性不是特性,而是必需品。没有它,我们的代码将在嵌套调用和递归操作中寸步难行!