ReentrantLock 可重入性设计:游乐园 VIP 通行证的秘密

45 阅读5分钟

将通过一个"游乐园 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();
        }
    }
}

​问题分析​​:

  1. 游客进入 rideRollerCoaster() 获取锁
  2. 在过山车上想调用 takePhotos()
  3. takePhotos() 再次尝试获取同一个锁
  4. 因为是不可重入锁,线程被阻塞等待自己释放锁 → ​​死锁!​

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游客体验流程​​:

  1. 第一次获取锁:state=0→1,记录持有者

  2. 在过山车上拍照(重入):

    • 检查持有者是当前线程 → 允许进入
    • state=1→2
  3. 在拍照时想再玩一次(再次重入):

    • state=2→3
  4. 释放过程:

    • 离开第二次游玩: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        | // 锁持有者
+------------------+-----------------+

获取锁的详细流程

image.png

释放锁的详细流程

image.png

可重入性设计注意事项

正确释放锁(配对解锁)

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​
首次获取15ns15ns
重入获取2ns死锁
内存开销24 bytes + Thread ref1 byte
递归操作支持不支持
方法嵌套支持导致死锁

总结:可重入性设计哲学

ReentrantLock 的可重入性设计解决了并发编程中的关键问题:

  1. ​避免自死锁​​:线程不会阻塞自己
  2. ​支持方法嵌套​​:同步方法可以自由调用其他同步方法
  3. ​实现递归操作​​:递归算法可以安全使用锁
  4. ​简化代码结构​​:符合面向对象设计原则

就像游乐园的 VIP 会员可以多次体验项目一样,可重入锁允许线程"深度进入"同步区域,使复杂业务逻辑的同步控制变得自然简单。这种设计体现了"同一个线程的重复访问是安全的"这一并发编程哲学,是构建健壮并发系统的基石。

最终,我们可以说:​​可重入性不是特性,而是必需品​​。没有它,我们的代码将在嵌套调用和递归操作中寸步难行!