ReentrantLock 是 JDK 提供的手动锁(位于 java.util.concurrent.locks 包),与 synchronized 同为可重入锁,但用法和特性有显著区别。下面从 用法、核心区别、适用场景 三个维度对比,讲清楚它们的“讲究”:
synchronized 是手动挡,ReentrantLock 是自动挡
一、基础用法对比
1. synchronized(隐式锁,JVM 内置)
无需手动加锁/解锁,由 JVM 自动管理锁的获取和释放(代码块执行完或抛异常时自动释放)。
// 同步代码块
Object lock = new Object();
synchronized (lock) {// 临界区代码(自动获取 lock 锁,执行完自动释放)
}// 同步方法(锁是 this 或 Class 对象)
public synchronized void method() {// 临界区代码
}
2. ReentrantLock(显式锁,手动控制)
需要手动调用 lock() 加锁、unlock() 解锁,且 unlock() 必须放在 finally 中(避免异常导致锁未释放)。
ReentrantLock lock = new ReentrantLock(); // 创建锁对象
try {lock.lock(); // 手动加锁// 临界区代码
} finally {lock.unlock(); // 手动解锁(必须在 finally 中,确保释放)
}
二、核心区别(重点“讲究”)
| 特性 | synchronized |
ReentrantLock |
|---|---|---|
| 锁的获取/释放 | 隐式(JVM 自动管理) | 显式(必须手动 lock()/unlock()) |
| 公平锁支持 | 不支持(默认非公平锁,无法设置) | 支持(构造器传入 true 开启公平锁) |
| 可中断获取 | 不支持(获取锁时被阻塞,无法被中断) | 支持(lockInterruptibly() 可响应中断) |
| 超时获取 | 不支持(获取不到锁会一直阻塞) | 支持(tryLock(long timeout, TimeUnit unit) 超时放弃) |
| 条件变量 | 仅支持一个等待队列(通过 wait() 等) |
支持多个条件变量(newCondition()),可按条件分组等待 |
| 性能 | JDK 1.6 后优化(偏向锁/轻量级锁),与 ReentrantLock 接近 |
早期性能优于 synchronized,现在差距不大 |
| 使用复杂度 | 简单(无需手动管理,不易出错) | 复杂(需手动释放,易漏写 unlock() 导致死锁) |
三、关键特性详解(为什么这些区别重要?)
1. 公平锁 vs 非公平锁
- 非公平锁(默认):线程获取锁时,不按等待顺序,谁先抢到谁先执行(效率高,但可能导致线程饥饿)。
- 公平锁:线程按等待顺序获取锁(先来后到),避免饥饿,但效率低(需维护等待队列)。
ReentrantLock 可通过构造器指定公平性:
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
ReentrantLock nonFairLock = new ReentrantLock(); // 非公平锁(默认)
synchronized 只能是非公平锁,无法设置。
2. 可中断获取锁
ReentrantLock 的 lockInterruptibly() 允许线程在等待锁时响应中断(比如“超时放弃”或“用户取消”):
ReentrantLock lock = new ReentrantLock();
Thread t = new Thread(() -> {try {lock.lockInterruptibly(); // 可被中断的加锁} catch (InterruptedException e) {System.out.println("线程被中断,放弃获取锁");return; // 中断后退出,避免死等}try {// 临界区代码} finally {lock.unlock();}
});
t.start();
Thread.sleep(1000);
t.interrupt(); // 中断线程 t,使其放弃等待锁
synchronized 无法做到——线程若在等待 synchronized 锁,中断信号会被忽略,继续死等。
3. 超时获取锁
ReentrantLock 的 tryLock() 可设置超时时间,避免线程永久阻塞:
ReentrantLock lock = new ReentrantLock();
if (lock.tryLock(3, TimeUnit.SECONDS)) { // 3秒内获取不到锁就放弃try {// 成功获取锁,执行逻辑} finally {lock.unlock();}
} else {// 获取锁失败,执行降级逻辑(如返回错误提示)
}
synchronized 没有超时机制,一旦开始等待,要么获取锁,要么一直阻塞。
4. 多条件变量
ReentrantLock 可通过 newCondition() 创建多个条件变量,实现“按条件分组等待”(比 synchronized 的单等待队列更灵活)。
示例(生产者-消费者的多条件场景):
假设一个仓库需要区分“库存不足等待”和“库存溢出等待”:
ReentrantLock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition(); // 条件1:库存非空(供消费者等)
Condition notFull = lock.newCondition(); // 条件2:库存未满(供生产者等)
int stock = 0;
int MAX = 10;// 生产者:库存满了就等 notFull 信号
public void produce() throws InterruptedException {lock.lock();try {while (stock >= MAX) {notFull.await(); // 等待“库存未满”信号}stock++;notEmpty.signal(); // 通知消费者“库存非空”} finally {lock.unlock();}
}// 消费者:库存空了就等 notEmpty 信号
public void consume() throws InterruptedException {lock.lock();try {while (stock <= 0) {notEmpty.await(); // 等待“库存非空”信号}stock--;notFull.signal(); // 通知生产者“库存未满”} finally {lock.unlock();}
}
synchronized 只能通过 wait()/notify() 在同一个锁对象上等待,无法按条件分组,灵活性差。
四、适用场景选择
-
优先用
synchronized的场景:- 简单同步逻辑(如单资源互斥),追求代码简洁、不易出错。
- 不需要额外特性(公平锁、超时、中断等),依赖 JVM 自动管理锁。
-
必须用
ReentrantLock的场景:- 需要公平锁(避免线程饥饿)。
- 需要中断等待锁的线程(如用户取消操作)。
- 需要超时获取锁(避免死锁)。
- 需要多条件变量(按不同条件分组等待)。
总结
synchronized 是“傻瓜式”锁(简单、安全,JVM 自动管理),ReentrantLock 是“专业级”锁(灵活、功能强,但需手动控制)。日常开发中,synchronized 足够应对大多数场景;当需要公平性、超时、中断等高级特性时,再用 ReentrantLock,且务必记得在 finally 中释放锁。
完整示范
下面是基于 ReentrantLock 多条件变量的生产者-消费者完整示例,包含详细注释和使用说明:
一、完整代码实现(仓库类 + 生产者/消费者线程)
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;/*** 基于 ReentrantLock 多条件变量的仓库类* 支持生产者生产商品、消费者消费商品,按"库存满/空"分组等待*/
public class Warehouse {private final ReentrantLock lock = new ReentrantLock(); // 锁对象private final Condition notFull; // 条件:库存未满(供生产者等待)private final Condition notEmpty; // 条件:库存非空(供消费者等待)private int stock; // 当前库存private final int MAX_CAPACITY; // 最大库存容量// 初始化仓库(指定最大库存)public Warehouse(int maxCapacity) {this.MAX_CAPACITY = maxCapacity;this.notFull = lock.newCondition(); // 创建"库存未满"条件this.notEmpty = lock.newCondition(); // 创建"库存非空"条件this.stock = 0; // 初始库存为0}/*** 生产者生产商品* 若库存已满,等待"库存未满"信号;生产后通知消费者"库存非空"*/public void produce(int num) throws InterruptedException {lock.lock(); // 手动加锁try {// 循环检查:若库存+待生产数量超过最大容量,等待while (stock + num > MAX_CAPACITY) {System.out.println("【生产者】库存不足(当前:" + stock + ",需生产:" + num + "),等待...");notFull.await(); // 等待"库存未满"信号(释放锁,进入等待队列)}// 执行生产stock += num;System.out.println("【生产者】生产了" + num + "个商品,当前库存:" + stock);// 生产后通知消费者:库存非空了notEmpty.signalAll(); // 唤醒所有等待"库存非空"的消费者} finally {lock.unlock(); // 手动解锁(必须在finally中,确保锁释放)}}/*** 消费者消费商品* 若库存为空,等待"库存非空"信号;消费后通知生产者"库存未满"*/public void consume(int num) throws InterruptedException {lock.lock(); // 手动加锁try {// 循环检查:若库存不足,等待while (stock < num) {System.out.println("【消费者】库存不足(当前:" + stock + ",需消费:" + num + "),等待...");notEmpty.await(); // 等待"库存非空"信号(释放锁,进入等待队列)}// 执行消费stock -= num;System.out.println("【消费者】消费了" + num + "个商品,当前库存:" + stock);// 消费后通知生产者:库存未满了notFull.signalAll(); // 唤醒所有等待"库存未满"的生产者} finally {lock.unlock(); // 手动解锁}}// 测试代码public static void main(String[] args) {// 创建一个最大容量为10的仓库Warehouse warehouse = new Warehouse(10);// 启动2个生产者线程(每个生产3次,每次生产2个商品)for (int i = 0; i < 2; i++) {new Thread(() -> {try {for (int j = 0; j < 3; j++) {warehouse.produce(2); // 每次生产2个Thread.sleep(500); // 模拟生产耗时}} catch (InterruptedException e) {e.printStackTrace();}}, "生产者-" + (i + 1)).start();}// 启动3个消费者线程(每个消费2次,每次消费3个商品)for (int i = 0; i < 3; i++) {new Thread(() -> {try {for (int j = 0; j < 2; j++) {warehouse.consume(3); // 每次消费3个Thread.sleep(800); // 模拟消费耗时}} catch (InterruptedException e) {e.printStackTrace();}}, "消费者-" + (i + 1)).start();}}
}
二、代码执行逻辑说明
-
核心组件:
ReentrantLock lock:全局锁,保证生产/消费操作的原子性。Condition notFull:生产者的等待条件(当库存满时,生产者在此等待)。Condition notEmpty:消费者的等待条件(当库存空时,消费者在此等待)。
-
生产流程:
- 生产者获取锁后,检查库存是否足够生产(若库存+待生产数量超过最大容量,则调用
notFull.await()释放锁并等待)。 - 生产完成后,调用
notEmpty.signalAll()唤醒所有等待“库存非空”的消费者。
- 生产者获取锁后,检查库存是否足够生产(若库存+待生产数量超过最大容量,则调用
-
消费流程:
- 消费者获取锁后,检查库存是否足够消费(若库存不足,则调用
notEmpty.await()释放锁并等待)。 - 消费完成后,调用
notFull.signalAll()唤醒所有等待“库存未满”的生产者。
- 消费者获取锁后,检查库存是否足够消费(若库存不足,则调用
三、执行结果示例(部分输出)
【生产者-1】生产了2个商品,当前库存:2
【生产者-2】生产了2个商品,当前库存:4
【消费者-1】消费了3个商品,当前库存:1
【消费者-2】消费了3个商品,库存不足(当前:1,需消费:3),等待...
【消费者-3】消费了3个商品,库存不足(当前:1,需消费:3),等待...
【生产者-1】生产了2个商品,当前库存:3
【生产者-2】生产了2个商品,当前库存:5
【消费者-2】消费了3个商品,当前库存:2
【消费者-3】消费了3个商品,库存不足(当前:2,需消费:3),等待...
...(后续按逻辑循环执行)
四、使用场景与注意事项
-
适用场景:
当需要按不同条件分组等待时(如“库存满”和“库存空”是两种独立条件),多条件变量比synchronized的单等待队列更高效(避免唤醒无关线程)。 -
使用注意事项:
lock()和unlock()必须配对,且unlock()务必放在finally中(防止异常导致锁未释放,引发死锁)。- 条件判断必须用
while而非if(防止“虚假唤醒”——线程可能在未被通知的情况下唤醒,需重新检查条件)。 signal()唤醒单个线程,signalAll()唤醒所有线程,根据场景选择(多生产者/消费者场景推荐signalAll(),避免遗漏)。
通过这个示例可以看到,ReentrantLock 的多条件变量机制能更精细地控制线程等待/唤醒,适合复杂的线程通信场景。
