简介: ReentrantLock是Java中比synchronized更灵活的同步工具,支持可中断、可超时、公平锁及多条件变量控制。本文深入解析其核心特性、Condition精准调度、底层AQS原理,并对比synchronized,助你掌握高并发编程的最佳实践。
- 引言
- 一、为什么需要ReentrantLock?
- 二、核心特性与使用方式
- 三、Condition:精准的线程调度
- 四、底层原理:AQS核心机制揭秘
- 五、总结与最佳实践
- 互动环节
引言

在Java并发编程中,synchronized关键字是我们的老朋友,它简单易用,但灵活性不足。当我们需要更复杂的同步控制时,比如尝试获取锁、可中断的锁获取、或者公平锁机制,该怎么办?
java.util.concurrent.locks.ReentrantLock(可重入锁)便是JDK为我们提供的强大答案!它不仅是synchronized的增强版,更是理解JUC包并发设计思想的钥匙。本文将带你深入探索ReentrantLock的奥秘,解锁高并发编程的新技能。
一、为什么需要ReentrantLock?
synchronized作为Java原生的同步机制,虽然简单,但存在一些局限性:
- 阻塞无法中断:线程如果无法获取锁,会一直阻塞下去,直到获取到锁。
- 不够灵活:只能使用单一的条件队列(wait/notify),无法实现复杂的线程协作。
- 非公平性:锁的获取默认是非公平的,可能导致线程“饥饿”。
ReentrantLock的设计目标:
- 可重入性:与synchronized一样,同一个线程可以多次获取同一把锁。
- 可中断:提供能响应中断的锁获取方式。
- 超时机制:可以尝试获取锁,如果一段时间内获取不到,可以放弃。
- 公平性选择:可以选择创建公平锁或非公平锁。
- 多条件变量:一个锁可以绑定多个Condition对象,实现更精细的线程等待/唤醒。
二、核心特性与使用方式
1. 基础用法
ReentrantLock的使用遵循一个固定模式:lock() -> try...finally -> unlock()。必须在finally块中释放锁,以确保异常时锁也能被释放。
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock(); // 手动获取锁(阻塞直到成功)
try {
count++; // 临界区代码
// 可重入性演示:同一个线程可以再次获取锁
if (count < 10) {
this.increment(); // 递归调用,会再次成功获取锁
}
} finally {
lock.unlock(); // 必须在finally中释放锁!
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
2. 尝试非阻塞获取锁 (tryLock)
这是ReentrantLock比synchronized灵活的重要体现。
public boolean tryIncrement() {
// 尝试获取锁,立即返回成功与否,不会阻塞线程
if (lock.tryLock()) {
try {
count++;
return true;
} finally {
lock.unlock();
}
} else {
// 没拿到锁,可以先去做别的事情
System.out.println("没能获取到锁,执行其他逻辑...");
return false;
}
}
public boolean tryIncrementWithTimeout() throws InterruptedException {
// 尝试获取锁,最多等待1秒
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
count++;
return true;
} finally {
lock.unlock();
}
} else {
System.out.println("1秒内都没获取到锁,放弃吧...");
return false;
}
}
3. 可中断的锁获取 (lockInterruptibly)
synchronized在获取锁时是不可中断的,而ReentrantLock提供了可中断的方式。
public void interruptibleIncrement() throws InterruptedException {
// 这个方法允许在等待锁的过程中被其他线程中断
lock.lockInterruptibly(); // 可能会抛出InterruptedException
try {
// 模拟一个长时间操作
while (true) {
count++;
Thread.sleep(1000); // 睡眠中可能被中断
}
} finally {
lock.unlock();
}
}
4. 公平锁 vs 非公平锁
- 非公平锁(默认):线程获取锁的顺序不严格按照请求的顺序,允许“插队”。吞吐量高,但可能造成线程“饥饿”。
- 公平锁:锁的分配严格按照FIFO(先进先出)原则,先请求的线程先获得锁。保证了公平性,但吞吐量相对较低。
// 创建公平锁 ReentrantLock fairLock = new ReentrantLock(true); // 创建非公平锁(默认) ReentrantLock nonFairLock = new ReentrantLock(); ReentrantLock nonFairLock2 = new ReentrantLock(false);
选择建议:除非有严格的公平性要求,否则默认使用非公平锁,因为其性能在大多数情况下优于公平锁。
三、Condition:精准的线程调度
synchronized与wait()/notifyAll()配合使用,所有等待线程都在同一个等待队列里,唤醒时不够精确。ReentrantLock可以与多个Condition(条件变量)关联,实现更精细的线程等待与唤醒。
经典案例:生产者-消费者模型
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ProducerConsumerModel {
private final Queue<Integer> queue = new LinkedList<>();
private final int capacity = 5;
private final ReentrantLock lock = new ReentrantLock();
// 为生产者创建一个条件队列:队列"未满"时才能生产
private final Condition notFull = lock.newCondition();
// 为消费者创建一个条件队列:队列"非空"时才能消费
private final Condition notEmpty = lock.newCondition();
public void produce(int item) throws InterruptedException {
lock.lock();
try {
// 使用while循环防止"虚假唤醒"
while (queue.size() == capacity) {
// 队列已满,生产者等待在"notFull"条件上
System.out.println("队列已满,生产者等待...");
notFull.await(); // 释放锁并等待,被唤醒后重新获取锁
}
queue.offer(item);
System.out.println("生产: " + item + ", 队列大小: " + queue.size());
// 生产了一个,队列肯定不空了,唤醒等待在"notEmpty"上的消费者
notEmpty.signalAll();
} finally {
lock.unlock();
}
}
public void consume() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
// 队列为空,消费者等待在"notEmpty"条件上
System.out.println("队列为空,消费者等待...");
notEmpty.await();
}
Integer item = queue.poll();
System.out.println("消费: " + item + ", 队列大小: " + queue.size());
// 消费了一个,队列肯定不满了,唤醒等待在"notFull"上的生产者
notFull.signalAll();
} finally {
lock.unlock();
}
}
}
使用多个Condition可以精准地唤醒某一类线程(如只唤醒生产者或只唤醒消费者),避免了notifyAll()唤醒所有线程带来的不必要的性能开销。
四、底层原理:AQS核心机制揭秘
ReentrantLock的强大功能,其核心实现依赖于一个强大的基础框架:
AbstractQueuedSynchronizer (AQS)。理解AQS是理解大部分JUC同步器的关键。
AQS的核心思想:
AQS内部维护了一个** volatile int state(同步状态)和一个FIFO线程等待队列**(CLH队列的变种)。
- state:对于ReentrantLock,state表示锁被重入的次数。为0时表示锁空闲,大于0时表示被线程持有。
- 等待队列:所有未能获取到锁的线程会被封装成Node节点,加入到这个队列中排队。
加锁过程(非公平锁为例):
- 首先尝试直接用CAS操作将state从0改为1。如果成功,则将当前线程设置为独占线程。
- 如果CAS失败,说明锁已被占用,则调用AQS的acquire方法。
- acquire会再次尝试获取锁(tryAcquire),失败后会将当前线程包装成Node节点,以自旋(循环尝试)的方式加入到等待队列的尾部。
- 在队列中,节点会不断检查前驱节点是否为头节点,如果是则再次尝试获取锁。如果不是,则可能被挂起(Park),等待前驱节点唤醒。
解锁过程:
- 调用unlock(),尝试释放锁(将state减1)。
- 如果state变为0,则唤醒等待队列中头节点的后继节点(下一个等待的线程)。
AQS通过这个模板,将复杂的线程排队、阻塞、唤醒机制封装起来,只暴露了tryAcquire、tryRelease等方法给子类(如ReentrantLock)去实现,这是一个经典的模板方法模式的应用。
五、总结与最佳实践
- vs synchronized:
- 特性synchronizedReentrantLock实现级别JVM关键字,内置JDK API,基于AQS锁释放自动释放必须手动unlock()灵活性基础丰富(可尝试、可中断、可公平)性能JDK6后优化很好,性能接近在高竞争下表现可能更好条件队列单一多个(Condition)
- 最佳实践:
- 首选synchronized:对于大多数简单的同步需求,优先使用synchronized,因为它更简单,不易出错(自动释放锁)。
- 需要高级功能时选择ReentrantLock:当你需要可中断的锁获取、超时机制、公平锁、或多个条件变量时,再选择ReentrantLock。
- 务必释放锁:使用ReentrantLock时,必须将unlock()操作放在finally块中,这是硬性规定!
- 谨慎使用公平锁:公平锁会带来性能开销,除非业务场景明确要求,否则不要使用。
ReentrantLock是Java并发工具库中一把强大的“瑞士军刀”,它提供了比synchronized更精细的控制能力。理解其原理和适用场景,能让你在面对复杂并发问题时拥有更多的选择和更强的解决能力。
171万+

被折叠的 条评论
为什么被折叠?



