当前位置: 首页 > news >正文

[java 锁 02 - synchronized vs ReentrantLock ]

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. 可中断获取锁

ReentrantLocklockInterruptibly() 允许线程在等待锁时响应中断(比如“超时放弃”或“用户取消”):

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. 超时获取锁

ReentrantLocktryLock() 可设置超时时间,避免线程永久阻塞:

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() 在同一个锁对象上等待,无法按条件分组,灵活性差。

四、适用场景选择

  1. 优先用 synchronized 的场景:

    • 简单同步逻辑(如单资源互斥),追求代码简洁、不易出错。
    • 不需要额外特性(公平锁、超时、中断等),依赖 JVM 自动管理锁。
  2. 必须用 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();}}
}

二、代码执行逻辑说明

  1. 核心组件

    • ReentrantLock lock:全局锁,保证生产/消费操作的原子性。
    • Condition notFull:生产者的等待条件(当库存满时,生产者在此等待)。
    • Condition notEmpty:消费者的等待条件(当库存空时,消费者在此等待)。
  2. 生产流程

    • 生产者获取锁后,检查库存是否足够生产(若库存+待生产数量超过最大容量,则调用 notFull.await() 释放锁并等待)。
    • 生产完成后,调用 notEmpty.signalAll() 唤醒所有等待“库存非空”的消费者。
  3. 消费流程

    • 消费者获取锁后,检查库存是否足够消费(若库存不足,则调用 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),等待...
...(后续按逻辑循环执行)

四、使用场景与注意事项

  1. 适用场景
    当需要按不同条件分组等待时(如“库存满”和“库存空”是两种独立条件),多条件变量比 synchronized 的单等待队列更高效(避免唤醒无关线程)。

  2. 使用注意事项

    • lock()unlock() 必须配对,且 unlock() 务必放在 finally 中(防止异常导致锁未释放,引发死锁)。
    • 条件判断必须用 while 而非 if(防止“虚假唤醒”——线程可能在未被通知的情况下唤醒,需重新检查条件)。
    • signal() 唤醒单个线程,signalAll() 唤醒所有线程,根据场景选择(多生产者/消费者场景推荐 signalAll(),避免遗漏)。

通过这个示例可以看到,ReentrantLock 的多条件变量机制能更精细地控制线程等待/唤醒,适合复杂的线程通信场景。

http://www.hskmm.com/?act=detail&tid=39440

相关文章:

  • 10.26
  • AI Agent 与 Agentic AI 系统:真正的区别是什么?
  • 2025 年 10 月门窗十大品牌榜单揭晓,聚焦专业制造与品牌口碑的品质之选
  • [LangChain] 09.LCEL
  • 2025年饮料包装设备厂家权威推荐榜:缠膜机/吹瓶机/膜包机/杀菌机/水处理/套标机/贴标机/洗瓶机/卸垛机/旋盖机/液氮机/装箱机/灌装生产线/一条龙生产线/配件/灌装机
  • 算法与数据结构 9 - 重链剖分
  • 域登录态分享(类sso)
  • Spring Cloud Gateway网关路由配置 - AlanLee
  • Alibaba Cloud Linux 4 安装docker后,修复docker的方法
  • 重构学习认知:从听讲、践行到教学的启示
  • ssh原理
  • 实现一个简易版本的IOC
  • day000 ML串讲
  • 我的学习方式破局思考 ——读《认真听讲》、《做中学》与《做教练》有感
  • cmd运行python文件
  • MPK(Mirage Persistent Kernel)源码笔记(2)--- 多层结构化图模型
  • Unity协程除了实现功能还可以增加可读性
  • 2025年TPU厂家权威推荐榜:专业TPU加纤、TPU改性生产技术实力与市场口碑深度解析
  • 作业一
  • Nginx部署星益小游戏平台(静态页面)
  • hadoop应用遇到的问题
  • Nginx程序结构及核心配置
  • 事倍功半是蠢蛋57 typora相对路径图片上传到github
  • 序列密码基本模型
  • 企业级Nginx安装部署
  • 2025 年 10 月门窗十大品牌综合实力权威推荐榜单,聚焦产能、专利与环保的实力品牌深度解析
  • 以“听”为基,以“做”为翼
  • 解码Linux文件IO之中文字库原理与应用
  • 企业级Web应用及Nginx介绍
  • 2025 年 10 月门窗十大品牌综合实力权威推荐榜单,精准检测与稳定性能兼具的行业优选解析