1. 基本概念

ReentrantLock 是 Java 并发包 (java.util.concurrent.locks) 中提供的一个可重入锁实现,是 Lock 接口的一个实现类。“可重入"意味着同一个线程可以多次获取同一把锁而不会死锁。

与 synchronized 关键字相比,ReentrantLock 提供了更加灵活和强大的锁机制:

  • 可中断锁获取
  • 可设置超时时间
  • 可实现公平锁
  • 可实现非块结构的加锁

2. 基本用法

Lock lock = new ReentrantLock();
try {
    // 获取锁
    lock.lock();
    // 临界区代码
    // ...
} finally {
    // 释放锁
    lock.unlock();
}

注意事项:

  • 必须在 finally 块中释放锁,确保锁一定会被释放
  • 锁的获取和释放必须成对出现

3. 高级特性

3.1 可中断锁获取

Lock lock = new ReentrantLock();
try {
    // 可中断地获取锁
    lock.lockInterruptibly();
    // 临界区代码
} catch (InterruptedException e) {
    // 处理中断异常
} finally {
    // 如果当前线程持有锁,则释放锁
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

3.2 超时锁获取

Lock lock = new ReentrantLock();
boolean acquired = false;
try {
    // 尝试在指定时间内获取锁
    acquired = lock.tryLock(5, TimeUnit.SECONDS);
    if (acquired) {
        // 成功获取锁,执行临界区代码
    } else {
        // 获取锁超时,执行替代逻辑
    }
} catch (InterruptedException e) {
    // 处理中断异常
} finally {
    // 如果获取了锁,则释放锁
    if (acquired) {
        lock.unlock();
    }
}

3.3 公平锁与非公平锁

ReentrantLock 默认创建的是非公平锁,可以通过构造函数参数创建公平锁:

// 创建公平锁
Lock fairLock = new ReentrantLock(true);
// 创建非公平锁(默认)
Lock unfairLock = new ReentrantLock(false);
  • 公平锁:线程按照请求锁的顺序获取锁,等待时间最长的线程优先获取锁
  • 非公平锁:线程可以"插队"获取锁,性能通常更好,但可能导致某些线程长时间等待(饥饿)

3.4 Condition 条件变量

ReentrantLock 可以创建多个条件变量,用于线程间的协作:

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

// 等待条件
lock.lock();
try {
    while (!conditionMet) {
        condition.await();
    }
    // 条件满足,执行操作
} finally {
    lock.unlock();
}

// 通知条件已满足
lock.lock();
try {
    // 改变条件状态
    conditionMet = true;
    condition.signal(); // 或 condition.signalAll();
} finally {
    lock.unlock();
}

4. 内部实现原理

4.1 基本结构

ReentrantLock 内部依赖于 AbstractQueuedSynchronizer (AQS) 实现,它有两个内部类:

  • FairSync:公平锁实现
  • NonfairSync:非公平锁实现

这两个类都继承自 AbstractQueuedSynchronizer,通过重写 AQS 的方法实现不同的锁语义。

4.2 AQS 核心原理

AQS 是 Java 并发包中的核心框架,提供了一个基于 FIFO 队列的阻塞锁和相关同步器的实现。AQS 的核心是:

  1. 一个 volatile 的 int 类型状态变量 (state)
  2. 一个 FIFO 的等待队列
  3. 线程阻塞和唤醒的机制

对于 ReentrantLock:

  • state = 0 表示锁未被占用
  • state > 0 表示锁被占用,值表示重入次数

4.3 锁获取过程

以非公平锁为例,获取锁的基本流程:

  1. 尝试通过 CAS 操作将 state 从 0 改为 1
  2. 如果成功,设置当前线程为锁的独占线程
  3. 如果失败,检查当前持有锁的线程是否是自己(重入情况)
    • 如果是,增加 state 值
    • 如果不是,将当前线程包装成 Node 加入等待队列,并阻塞当前线程

公平锁的区别在于,它会先检查队列中是否有等待的线程,如果有则不会尝试获取锁,而是直接进入队列等待。

4.4 锁释放过程

  1. 减少 state 值
  2. 当 state 变为 0 时,清除独占线程标记
  3. 唤醒等待队列中的下一个线程

4.5 可重入实现

ReentrantLock 的可重入是通过记录获取锁的线程标识和重入次数实现的:

  • 当一个线程获取锁时,记录这个线程的标识(Thread.currentThread())
  • 同一个线程再次获取锁时,检查是否是已持有锁的线程,如果是则增加重入计数
  • 释放锁时,减少重入计数,当计数为 0 时才真正释放锁

5. ReentrantLock vs synchronized

特性 ReentrantLock synchronized
可重入性 支持 支持
锁释放 显式调用 unlock() 自动释放
公平性 可选择公平/非公平 非公平
中断响应 支持 不支持
超时获取 支持 不支持
多条件变量 支持 不支持(只有一个条件变量)
性能 JDK 1.6 后与 synchronized 接近 JDK 1.6 后进行了优化
使用难度 稍复杂,需要手动释放锁 简单,语法层面支持

6. 使用场景

ReentrantLock 适用于以下场景:

  • 需要高级功能如可中断、超时获取、公平性的场景
  • 需要多个条件变量的场景
  • 需要非阻塞地尝试获取锁的场景
  • 需要实现可中断的锁获取操作的场景

synchronized 适用于:

  • 简单的同步需求
  • 不需要高级特性的场景
  • 追求代码简洁性的场景

7. 最佳实践

  1. 始终在 finally 块中释放锁
  2. 避免在持有锁的情况下调用外部方法
  3. 尽量减少锁的持有时间
  4. 优先考虑使用 synchronized,除非需要 ReentrantLock 的特殊功能
  5. 使用 try-with-resources 简化锁的管理(Java 8+)
// 实现 AutoCloseable 接口的锁包装类
class AutoCloseableLock implements AutoCloseable {
    private final Lock lock;
    
    public AutoCloseableLock(Lock lock) {
        this.lock = lock;
        this.lock.lock();
    }
    
    @Override
    public void close() {
        this.lock.unlock();
    }
}

// 使用示例
Lock lock = new ReentrantLock();
try (AutoCloseableLock l = new AutoCloseableLock(lock)) {
    // 临界区代码
}

8. 总结

ReentrantLock 是 Java 并发编程中一个强大的工具,它提供了比 synchronized 更多的功能和灵活性。通过深入理解其内部实现原理和使用方法,可以在适当的场景下选择使用它来解决复杂的并发问题。

然而,在大多数简单的同步场景下,synchronized 关键字仍然是首选,因为它使用更简单,且 JVM 对其进行了大量优化。只有在确实需要 ReentrantLock 提供的高级特性时,才应该选择使用它。