Skip to content

一、ReentrantLock 基础概述

ReentrantLock 是 Java 并发包(java.util.concurrent.locks)中提供的一种可重入的互斥锁实现,它实现了 Lock 接口,提供了比 synchronized 更灵活、更强大的同步机制。

1.1 ReentrantLock 的核心特性

  • 可重入性:支持同一个线程多次获取锁
  • 可中断性:支持中断等待锁的线程
  • 公平性选择:支持公平锁和非公平锁两种模式
  • 条件变量支持:通过 Condition 接口实现更灵活的线程间通信
  • 显式获取和释放:需要手动调用 lock()unlock() 方法

1.2 ReentrantLock 的使用场景

  • 需要可中断锁的场景
  • 需要公平锁的场景
  • 需要多个条件变量的场景
  • 需要尝试获取锁(tryLock)的场景
  • 需要超时获取锁的场景

二、ReentrantLock 的基本使用

2.1 创建 ReentrantLock 实例

java
// 创建非公平锁(默认)
ReentrantLock lock = new ReentrantLock();

// 创建公平锁
ReentrantLock fairLock = new ReentrantLock(true);

2.2 基本的锁获取和释放

java
ReentrantLock lock = new ReentrantLock();

// 获取锁
lock.lock();
try {
    // 同步代码块
    // 访问共享资源
} finally {
    // 释放锁,必须在finally块中执行
    lock.unlock();
}

注意:释放锁的操作必须放在 finally 块中,以确保即使发生异常,锁也能被正确释放。

三、ReentrantLock 的高级功能

3.1 可中断的锁获取

lockInterruptibly() 方法允许线程在等待锁的过程中被中断:

java
ReentrantLock lock = new ReentrantLock();

Thread thread = new Thread(() -> {
    try {
        // 可被中断的锁获取
        lock.lockInterruptibly();
        try {
            // 同步代码块
        } finally {
            lock.unlock();
        }
    } catch (InterruptedException e) {
        // 处理中断
        Thread.currentThread().interrupt();
    }
});

thread.start();
// 中断线程
thread.interrupt();

3.2 尝试获取锁

tryLock() 方法尝试获取锁,如果获取成功返回 true,否则返回 false,不会阻塞:

java
ReentrantLock lock = new ReentrantLock();

if (lock.tryLock()) {
    try {
        // 同步代码块
    } finally {
        lock.unlock();
    }
} else {
    // 获取锁失败的处理逻辑
}

3.3 带超时的锁获取

tryLock(long timeout, TimeUnit unit) 方法尝试在指定时间内获取锁:

java
ReentrantLock lock = new ReentrantLock();

try {
    if (lock.tryLock(5, TimeUnit.SECONDS)) {
        try {
            // 同步代码块
        } finally {
            lock.unlock();
        }
    } else {
        // 超时处理逻辑
    }
} catch (InterruptedException e) {
    // 处理中断
    Thread.currentThread().interrupt();
}

3.4 条件变量

ReentrantLock 可以通过 newCondition() 方法创建多个条件变量,用于线程间通信:

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

// 等待条件满足
lock.lock();
try {
    while (!conditionSatisfied()) {
        // 当前线程释放锁并等待
        condition.await();
    }
    // 条件满足后的处理逻辑
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
} finally {
    lock.unlock();
}

// 通知等待的线程条件已满足
lock.lock();
try {
    // 更新条件状态
    updateCondition();
    // 唤醒一个等待的线程
    condition.signal();
    // 或者唤醒所有等待的线程
    // condition.signalAll();
} finally {
    lock.unlock();
}

四、ReentrantLock 的实现原理

4.1 基于 AQS 的实现

ReentrantLock 基于 Java 并发包中的 AbstractQueuedSynchronizer (AQS) 框架实现,主要包含以下核心组件:

  • 同步器(Sync)ReentrantLock 的内部抽象类,继承自 AQS,实现了锁的基本操作
  • 非公平同步器(NonfairSync):实现非公平锁的逻辑
  • 公平同步器(FairSync):实现公平锁的逻辑

4.2 锁的获取机制

  1. 非公平锁:线程尝试获取锁时,首先会尝试直接获取,如果成功则获取锁;否则,才会进入等待队列。
  2. 公平锁:线程必须按照请求锁的顺序来获取锁,先到先得。

4.3 可重入性的实现

ReentrantLock 通过在 AQS 的 state 变量中记录持有锁的线程获取锁的次数来实现可重入性:

  • 当线程第一次获取锁时,将 state 设为 1
  • 当同一线程再次获取锁时,将 state 加 1
  • 当线程释放锁时,将 state 减 1
  • state 减为 0 时,表示锁被完全释放

4.4 等待队列管理

ReentrantLock 使用 AQS 提供的 CLH (Craig, Landin, and Hagersten) 队列来管理等待锁的线程。线程在获取锁失败后会被包装成节点加入队列,按照 FIFO (First In First Out) 的顺序等待锁的释放。

五、ReentrantLock 与 synchronized 的比较

特性ReentrantLocksynchronized
获取方式显式通过 lock()/unlock() 获取和释放隐式获取和释放
可中断性支持可中断锁(lockInterruptibly())不支持中断
公平性支持公平锁和非公平锁仅支持非公平锁
条件变量支持多个条件变量不支持多条件变量
锁释放必须手动释放(通常在 finally 块中)自动释放(即使发生异常)
超时机制支持超时获取锁不支持超时
尝试获取支持尝试获取锁(tryLock())不支持尝试获取
性能在高并发下性能略好JDK 6 后性能大幅提升,接近 ReentrantLock

六、ReentrantLock 的使用最佳实践

6.1 始终在 finally 块中释放锁

这是使用 ReentrantLock 最重要的规则,确保即使发生异常,锁也能被正确释放:

java
ReentrantLock lock = new ReentrantLock();

lock.lock();
try {
    // 同步代码块
} finally {
    // 必须在finally中释放锁
    lock.unlock();
}

6.2 合理选择公平锁和非公平锁

  • 非公平锁(默认):性能更好,但可能导致线程饥饿
  • 公平锁:确保线程按照请求顺序获取锁,但性能略差

通常情况下,非公平锁已经足够满足大多数场景的需求:

java
// 非公平锁(性能更好,推荐使用)
ReentrantLock lock = new ReentrantLock();

// 只有在确实需要公平性保证时才使用公平锁
ReentrantLock fairLock = new ReentrantLock(true);

6.3 适当使用可中断锁

在需要响应中断的场景下,使用 lockInterruptibly() 替代 lock()

java
try {
    lock.lockInterruptibly();
    try {
        // 同步代码块
    } finally {
        lock.unlock();
    }
} catch (InterruptedException e) {
    // 处理中断
    Thread.currentThread().interrupt();
}

6.4 利用超时机制避免死锁

在获取锁时设置合理的超时时间,可以有效避免死锁:

java
try {
    if (lock.tryLock(5, TimeUnit.SECONDS)) {
        try {
            // 同步代码块
        } finally {
            lock.unlock();
        }
    } else {
        // 超时处理逻辑
        log.warn("Failed to acquire lock in 5 seconds");
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

6.5 合理使用条件变量

使用条件变量实现线程间通信时,注意以下几点:

  1. 总是在循环中检查条件,而不是在 if 语句中
  2. 确保在调用 signal()signalAll() 之前更新了条件状态
  3. 避免过早或过晚唤醒等待线程
java
// 正确的条件等待模式
lock.lock();
try {
    while (!conditionSatisfied) {
        condition.await();
    }
    // 处理逻辑
} finally {
    lock.unlock();
}

七、ReentrantLock 的常见问题与解决方案

7.1 忘记释放锁

问题:忘记在 finally 块中释放锁,可能导致死锁

解决方案:严格遵循在 finally 块中释放锁的最佳实践

7.2 锁释放次数少于获取次数

问题:由于可重入性,可能导致锁释放次数少于获取次数,造成锁未完全释放

解决方案:确保每一次 lock() 调用都有对应的 unlock() 调用

java
// 错误示例
lock.lock();
lock.lock();
try {
    // 处理逻辑
} finally {
    lock.unlock(); // 只释放了一次,锁未完全释放
}

// 正确示例
lock.lock();
lock.lock();
try {
    // 处理逻辑
} finally {
    lock.unlock();
    lock.unlock(); // 释放两次,锁完全释放
}

7.3 线程饥饿

问题:在非公平锁模式下,可能导致某些线程长期无法获取锁

解决方案

  • 对于确实需要公平性的场景,使用公平锁
  • 合理设计代码结构,减少锁的持有时间

7.4 条件变量使用不当导致的问题

问题:在 if 语句中检查条件而不是在循环中,可能导致虚假唤醒问题

解决方案:始终在循环中检查条件

java
// 错误示例
if (!conditionSatisfied) {
    condition.await();
}

// 正确示例
while (!conditionSatisfied) {
    condition.await();
}

八、ReentrantLock 的高级应用示例

8.1 实现生产者-消费者模式

java
public class BlockingQueue<T> {
    private final Queue<T> queue;
    private final int capacity;
    private final ReentrantLock lock;
    private final Condition notEmpty;
    private final Condition notFull;

    public BlockingQueue(int capacity) {
        this.queue = new LinkedList<>();
        this.capacity = capacity;
        this.lock = new ReentrantLock();
        this.notEmpty = lock.newCondition();
        this.notFull = lock.newCondition();
    }

    public void put(T element) throws InterruptedException {
        lock.lockInterruptibly();
        try {
            // 队列满时等待
            while (queue.size() == capacity) {
                notFull.await();
            }
            
            queue.offer(element);
            // 通知等待的消费者
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public T take() throws InterruptedException {
        lock.lockInterruptibly();
        try {
            // 队列空时等待
            while (queue.isEmpty()) {
                notEmpty.await();
            }
            
            T element = queue.poll();
            // 通知等待的生产者
            notFull.signal();
            return element;
        } finally {
            lock.unlock();
        }
    }
}

8.2 实现可重入读写锁

java
public class ReentrantReadWriteLockExample {
    private final ReentrantLock readLock = new ReentrantLock();
    private final ReentrantLock writeLock = new ReentrantLock();
    private int readCount = 0;
    private final Object readCountLock = new Object();

    public void read() {
        readLock.lock();
        try {
            synchronized (readCountLock) {
                readCount++;
                if (readCount == 1) {
                    // 第一个读线程获取写锁
                    writeLock.lock();
                }
            }
        } finally {
            readLock.unlock();
        }

        try {
            // 读取操作
            System.out.println("Reading data...");
        } finally {
            readLock.lock();
            try {
                synchronized (readCountLock) {
                    readCount--;
                    if (readCount == 0) {
                        // 最后一个读线程释放写锁
                        writeLock.unlock();
                    }
                }
            } finally {
                readLock.unlock();
            }
        }
    }

    public void write() {
        writeLock.lock();
        try {
            // 写入操作
            System.out.println("Writing data...");
        } finally {
            writeLock.unlock();
        }
    }
}

九、总结

ReentrantLock 是 Java 并发编程中功能强大的同步机制,提供了比 synchronized 更灵活的锁操作和控制能力。在需要可中断锁、公平锁、超时获取锁、多条件变量等高级特性的场景下,ReentrantLock 是更好的选择。

使用 ReentrantLock 时,务必遵循最佳实践,特别是始终在 finally 块中释放锁,以确保锁的正确管理和程序的健壮性。在大多数简单的同步场景下,synchronized 仍然是一个不错的选择,因为它使用更简单,且无需手动释放锁。

基于 MIT 许可发布