ReentrantLock深度解析(设计思想、底层原理、常见问题)

415 阅读5分钟

知其然要知其所以然,探索每一个知识点背后的意义,你知道的越多,你不知道的越多,一起学习,一起进步,如果文章感觉对您有用的话,关注、收藏、点赞,有困惑的地方可以评论,我们一起探讨!

ReentrantLock 深度分析

ReentrantLock 是 Java 并发包(java.util.concurrent.locks)中的核心类,实现了 可重入互斥锁,相比 synchronized 关键字,提供了更灵活的锁控制、可中断性、公平锁策略和多个条件变量支持。以下是其设计思想、实现原理及最佳实践的系统分析。


一、核心设计思想

1. 可重入性(Reentrancy)

  • 定义:同一线程可多次获取同一把锁,避免自身死锁。
  • 实现:通过计数器记录锁的持有次数,释放时需完全解锁(计数器归零)。
  • 示例
    ReentrantLock lock = new ReentrantLock();
    lock.lock();
    try {
        lock.lock(); // 可重入,计数器+1
        // 临界区代码
    } finally {
        lock.unlock(); // 计数器-1
        lock.unlock(); // 计数器归零,释放锁
    }
    

2. 公平性(Fairness)

  • 公平锁:锁的获取按请求顺序(FIFO),避免线程饥饿。
  • 非公平锁(默认):允许插队,减少线程切换开销,提高吞吐量。
  • 选择策略
    • 高竞争场景:公平锁减少饥饿,但性能较低。
    • 低竞争场景:非公平锁性能更优。

3. 显式锁控制

  • 手动加锁/解锁:需在 try-finally 块中确保锁释放,避免死锁。
  • 灵活性:支持尝试获取锁(tryLock)、超时获取(lockInterruptibly)、可中断等待等。

二、底层实现:AQS(AbstractQueuedSynchronizer)

ReentrantLock 的核心依赖于 AQS 框架,通过 CLH 队列(Craig, Landin, Hagersten 队列)管理线程的阻塞与唤醒。

1. AQS 的核心机制

  • 状态变量(state)
    • 对于 ReentrantLockstate 表示锁的持有计数(0 表示未锁定,≥1 表示锁定次数)。
  • CLH 队列
    • 双向链表结构,保存等待锁的线程。
    • 每个节点(Node)封装线程的等待状态(如取消、唤醒信号)。

2. 加锁流程(以非公平锁为例)

sequenceDiagram
    participant Thread as 线程
    participant AQS as AQS状态机
    participant CLH as CLH队列

    Thread->>AQS: 1. 尝试CAS设置state(直接获取锁)
    alt CAS成功(state=0→1)
        AQS-->>Thread: 获取锁成功,设置当前线程为持有者
    else CAS失败
        Thread->>AQS: 2. 尝试再次获取锁(可能插队)
        alt 再次CAS成功
            AQS-->>Thread: 获取锁成功
        else
            Thread->>AQS: 3. 将线程包装为Node加入CLH队列尾部
            AQS->>CLH: 加入队列
            loop 自旋或阻塞
                CLH->>AQS: 4. 检查前驱节点是否为头节点
                AQS->>Thread: 5. 若前驱是头节点,尝试CAS获取锁
                alt 获取成功
                    AQS->>CLH: 移除当前节点,设为头节点
                    AQS-->>Thread: 获取锁成功
                else
                    AQS->>Thread: 6. 挂起线程(LockSupport.park)
                end
            end
        end
    end

3. 解锁流程

public void unlock() {
    sync.release(1); // 调用AQS的release方法
}

// AQS的release方法
public final boolean release(int arg) {
    if (tryRelease(arg)) { // 尝试释放锁(由ReentrantLock.Sync实现)
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h); // 唤醒CLH队列中的下一个线程
        return true;
    }
    return false;
}

三、公平锁 vs 非公平锁的源码差异

1. 非公平锁(NonfairSync)

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) { // 直接尝试CAS,允许插队
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) { // 可重入
        setState(c + acquires);
        return true;
    }
    return false;
}

2. 公平锁(FairSync)

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() && // 检查CLH队列是否有等待线程
            compareAndSetState(0, acquires)) { // 无等待线程才尝试CAS
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) { // 可重入
        setState(c + acquires);
        return true;
    }
    return false;
}

四、高级功能与使用场景

1. 可中断锁获取(lockInterruptibly

ReentrantLock lock = new ReentrantLock();
try {
    lock.lockInterruptibly(); // 可响应中断
    try {
        // 临界区代码
    } finally {
        lock.unlock();
    }
} catch (InterruptedException e) {
    // 处理中断
}

2. 超时尝试获取锁(tryLock

if (lock.tryLock(1, TimeUnit.SECONDS)) { // 等待1秒
    try {
        // 临界区代码
    } finally {
        lock.unlock();
    }
} else {
    // 超时处理
}

3. 条件变量(Condition

  • synchronizedwait/notify 对比
    • 一个 ReentrantLock 可创建多个 Condition,实现精细化的线程等待/唤醒。
    • 示例:生产者-消费者模型。
    ReentrantLock lock = new ReentrantLock();
    Condition notFull = lock.newCondition();  // 队列未满条件
    Condition notEmpty = lock.newCondition(); // 队列非空条件
    
    // 生产者
    lock.lock();
    try {
        while (queue.isFull()) {
            notFull.await(); // 等待队列未满
        }
        queue.add(item);
        notEmpty.signal(); // 唤醒消费者
    } finally {
        lock.unlock();
    }
    
    // 消费者
    lock.lock();
    try {
        while (queue.isEmpty()) {
            notEmpty.await(); // 等待队列非空
        }
        item = queue.remove();
        notFull.signal(); // 唤醒生产者
    } finally {
        lock.unlock();
    }
    

五、最佳实践与常见问题

1. 必须手动释放锁

  • 错误示例
    lock.lock();
    // 若此处抛出异常,锁无法释放!
    lock.unlock();
    
  • 正确做法:始终在 finally 块中释放锁。

2. 避免嵌套锁

  • 死锁风险
    lockA.lock();
    try {
        lockB.lock(); // 若另一线程以相反顺序获取锁,可能死锁
        // ...
    } finally {
        lockB.unlock();
        lockA.unlock();
    }
    
  • 解决:按全局固定顺序获取锁。

3. 性能调优

  • 选择非公平锁:默认策略,适合大多数场景。
  • 减少锁粒度:结合分段锁(如 ConcurrentHashMap)。
  • 监控锁竞争:使用 jstackJFR 分析线程阻塞情况。

六、ReentrantLock vs synchronized

特性ReentrantLocksynchronized
实现方式JDK 类,基于 AQSJVM 内置关键字
锁获取方式显式调用 lock()/unlock()隐式获取(代码块/方法)
公平性支持公平/非公平锁仅非公平锁
可中断性支持(lockInterruptibly不支持
超时机制支持(tryLock不支持
条件变量支持多个 Condition单一 wait/notify
性能高竞争下更优(可配置策略)Java 6 后优化,低竞争下性能接近
代码复杂度高(需手动管理锁)低(自动释放)

七、源码分析:AQS 的等待队列

// AQS 中的 Node 类(简化版)
static final class Node {
    volatile int waitStatus;      // 等待状态(CANCELLED、SIGNAL等)
    volatile Node prev;           // 前驱节点
    volatile Node next;           // 后继节点
    volatile Thread thread;       // 关联的线程
    Node nextWaiter;              // 条件队列中的下一个节点
}

// CLH 队列示意图
Head -> Node(Thread1, SIGNAL) ↔ Node(Thread2, CANCELLED) ↔ Tail

八、总结

ReentrantLock 的设计体现了 灵活性可扩展性

  1. 基于 AQS 的模板方法模式:将锁的获取/释放逻辑委托给子类实现。
  2. 分离公平性与非公平性:通过不同的 Sync 实现类支持不同策略。
  3. 条件变量的精细化控制:解决 synchronizedwait/notify 的局限性。

适用场景

  • 需要可中断、超时或公平锁的高并发场景。
  • 需要多个条件变量实现复杂线程协作(如线程池任务队列)。