发布时间:2026/9/5 9:30:26
Java synchronized 与 ReentrantLock 到底怎么选:可中断、tryLock 超时、公平锁与 Condition 精准唤醒 Java synchronized 与 ReentrantLock 到底怎么选:可中断、tryLock 超时、公平锁与 Condition 精准唤醒写并发代码加锁,新手清一色用synchronized——简单,一个关键字搞定。但工作几年后你会遇到一堆synchronized干不了的场景:线程卡在锁上想中断它、抢不到锁不想干等而是超时放弃、要区分「读多写少」、要精确唤醒某一类等待线程。这时候ReentrantLock就该上场了。这篇文章不堆 API,而是从「synchronized具体卡在哪」出发,一个个用ReentrantLock的能力去补,最后给一条清晰的选型结论。先把 synchronized 的定位说清楚synchronized不是过时的东西,恰恰相反,大多数场景它就是最优解。原因:publicclassCounter{privateintcount;// 方法级:锁的是 thispublicsynchronizedvoidinc(){count;}// 代码块级:锁的是指定对象,粒度更细privatefinalObjectlocknewObject();publicvoidincBlock(){synchronized(lock){count;}}}它的优点是:JVM 内建、无需手动释放(异常了也自动解锁)、JIT 对它有偏向锁/轻量级锁等优化。只要你的需求是「简单互斥」,别折腾,用synchronized。它的局限在于「不可控」:一旦一个线程没抢到锁,就只能死等,你没有任何手段干预。下面四个场景就是它的天花板。局限一:抢不到锁想放弃 → tryLocksynchronized抢锁是「要么拿到、要么阻塞」,没有第三条路。但现实里经常需要「试着拿一下,拿不到就干别的,别在这耗着」。比如一个定时任务,上一轮还没跑完就跳过本轮,而不是排队堆积。ReentrantLock.tryLock()提供了这个能力:importjava.util.concurrent.locks.ReentrantLock;publicclassTask{privatefinalReentrantLocklocknewReentrantLock();publicvoidrunIfIdle(){// 试着拿锁,拿不到立刻返回 false,不阻塞if(!lock.tryLock()){System.out.println(上一轮还在跑,本轮跳过);return;}try{doWork();}finally{lock.unlock();// 必须放 finally,否则异常了锁泄漏}}privatevoiddoWork(){/* ... */}}还能带超时:「最多等 2 秒,还拿不到就放弃」:importjava.util.concurrent.TimeUnit;publicbooleanrunWithTimeout()throwsInterruptedException{// 最多等 2 秒if(!lock.tryLock(2,TimeUnit.SECONDS)){returnfalse;// 超时,放弃}try{doWork();returntrue;}finally{lock.unlock();}}这里有个必须记死的纪律:ReentrantLock要手动unlock,而且必须放在finally里。synchronized是 JVM 帮你在退出(含异常)时自动解锁,ReentrantLock没这待遇,忘了unlock或者中途抛异常没进 finally,锁就永远不释放,后面所有线程全卡死。这是从synchronized转过来最容易翻的车。局限二:线程卡在锁上想中断它 → lockInterruptibly被synchronized阻塞的线程无法响应中断——你thread.interrupt()它,它照样在那儿等着。这在需要「优雅停机」「取消长时间等待」的场景很致命。ReentrantLock.lockInterruptibly()让等锁的线程可以被中断:publicvoiddoInterruptibly()throwsInterruptedException{// 等锁期间若被 interrupt(),抛 InterruptedException 跳出lock.lockInterruptibly();try{doWork();}finally{lock.unlock();}}配合线程池优雅关闭:shutdownNow()会给工作线程发中断,用lockInterruptibly()的线程就能从等锁状态里跳出来收尾退出,而用synchronized的线程会继续傻等,拖住整个关闭流程。局限三:读多写少想并发读 → 换 ReadWriteLock严格说这不是ReentrantLock的功能,而是它的兄弟ReentrantReadWriteLock,但选型时一起考虑。synchronized和ReentrantLock都是「互斥锁」:哪怕两个线程都只是读,也要排队。对于「读远多于写」的缓存类场景,这是巨大的浪费。importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantReadWriteLock;importjava.util.HashMap;importjava.util.Map;publicclassCache{privatefinalMapString,StringmapnewHashMap();privatefinalReentrantReadWriteLockrwLocknewReentrantReadWriteLock();// 字段不能用 var,老实写 Lock 类型privatefinalLockreadLockrwLock.readLock();privatefinalLockwriteLockrwLock.writeLock();publicStringget(Stringkey){readLock.lock();// 多个读线程可同时持有读锁try{returnmap.get(key);}finally{readLock.unlock();}}publicvoidput(Stringkey,Stringvalue){writeLock.lock();// 写锁独占,期间所有读写都挡住try{map.put(key,value);}finally{writeLock.unlock();}}}读锁共享、写锁独占。读多写少时并发度远高于普通互斥锁。但注意:如果写并不罕见,读写锁的开销和写饥饿问题可能反而不如直接用ConcurrentHashMap。选它的前提是明确的「读 写」。局限四:精确唤醒某一类等待线程 → Conditionsynchronized配wait()/notify()/notifyAll()做线程协作时,有个硬伤:只有一个「等待队列」。生产者-消费者模型里,队列满时生产者等待、队列空时消费者等待,它们共用同一个等待集合,你只能notifyAll()全叫醒,然后大家抢着重新判断条件,效率低还容易惊群。ReentrantLock能创建多个Condition,每类等待线程各用一个,精确唤醒:importjava.util.concurrent.locks.Condition;importjava.util.concurrent.locks.ReentrantLock;publicclassBoundedBufferT{privatefinalObject[]items;privateintcount,putIdx,takeIdx;privatefinalReentrantLocklocknewReentrantLock();privatefinalConditionnotFulllock.newCondition();// 生产者等这个privatefinalConditionnotEmptylock.newCondition();// 消费者等这个publicBoundedBuffer(intcap){itemsnewObject[cap];}publicvoidput(Tx)throwsInterruptedException{lock.lock();try{while(countitems.length){notFull.await();// 满了,生产者在 notFull 上等}items[putIdx]x;putIdx(putIdx1)%items.length;count;notEmpty.signal();// 只唤醒一个消费者,不惊动其他生产者}finally{lock.unlock();}}SuppressWarnings(unchecked)publicTtake()throwsInterruptedException{lock.lock();try{while(count0){notEmpty.await();// 空了,消费者在 notEmpty 上等}Tx(T)items[takeIdx];takeIdx(takeIdx1)%items.length;count--;notFull.signal();// 只唤醒一个生产者returnx;}finally{lock.unlock();}}}put时只notEmpty.signal()唤醒消费者,take时只notFull.signal()唤醒生产者,各叫各的,没有惊群。这是synchronized单一等待队列做不到的。注意await()依然要放在while循环里判断条件(防虚假唤醒),这点和wait()一样。顺带一提:公平锁ReentrantLock构造时传true可以要公平锁,synchronized只能是非公平的:// 公平锁:先到先得,严格按等待顺序拿锁privatefinalReentrantLockfairLocknewReentrantLock(true);// 默认非公平:允许插队,吞吐更高privatefinalReentrantLocklocknewReentrantLock();公平锁避免了线程饥饿(某个倒霉线程一直抢不到),但代价是吞吐量明显下降——因为要严格维护队列顺序、频繁上下文切换。默认(非公平)几乎总是更好的选择,只有在明确观察到「某些线程长期饿死」且业务不能接受时,才考虑公平锁。别一上来就new ReentrantLock(true),那通常是负优化。选型结论把上面的分析压缩成一张决策表:你的需求该用什么简单互斥,拿不到就等synchronized(默认首选)抢不到锁要放弃/超时ReentrantLock.tryLock()等锁期间要能被中断ReentrantLock.lockInterruptibly()读远多于写ReentrantReadWriteLock要精确唤醒某类等待线程ReentrantLock 多个Condition要严格 FIFO 防饥饿ReentrantLock(true)公平锁一句话:没有上面那些特殊需求,就用synchronized;需要「可中断、可超时、可尝试、多条件、公平」中的任何一个,才换ReentrantLock。别因为「听说 Lock 更高级」就无脑替换——现代 JVM 对synchronized优化得很好,而ReentrantLock那个「必须手动 finally unlock」的负担,是实打实会引入锁泄漏 bug 的。小结synchronized是默认首选:JVM 内建、自动释放、JIT 优化好,简单互斥场景没有理由不用它。ReentrantLock的价值在于四个synchronized干不了的能力:tryLock(可尝试/超时)、lockInterruptibly(可中断)、多Condition(精确唤醒)、公平锁选项。用ReentrantLock必须把unlock()放进finally,否则异常路径会锁泄漏——这是它相比synchronized唯一但致命的心智负担。读多写少考虑ReentrantReadWriteLock,但写不罕见时可能不如直接上ConcurrentHashMap;公平锁默认别开,它是负优化的常见来源。记忆点:能力越强,责任越大——ReentrantLock给你更多控制权,也把「记得解锁」的责任交还给了你。按需求选,不按「谁更高级」选。

相关新闻

2026/9/5 9:30:26

Redis单线程高性能原理深度解析:从I/O多路复用到架构权衡

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/5 9:25:25

重卡充电站怎么选址?能效电气用S1200和S2500来打样

2026年,新能源重卡市场迎来了真正的爆发时刻。根据交强险数据,2025年12月,新能源重卡渗透率已提升至53.89%,全年销量达到23.32万辆,同比增长181.91%。预计2026年,新能源重卡平均渗透率有望突破35%&#xff…

2026/9/5 9:25:25

从双击到内核:一次文件打开背后的操作系统原理

从双击到内核:一次文件打开背后的操作系统原理文件系统的基本全貌(宏观视角)1.1 文件与文件系统文件也有一些分类。按逻辑结构分类如下所示。图1 文件逻辑结构分类标题文件系统,首先包括的当然是当前存储容器中的所有文件。如果仅…

2026/9/5 10:25:33

VMP软件保护原理与逆向分析实战:从虚拟化机制到脱壳思路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/5 10:25:33

文创内容推荐系统:SpringBoot+Vue轻量级混合推荐实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/5 10:20:32

AI开发者工作负荷管理:从Jason Liu暂停更新看可持续开发策略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/5 2:46:54

vSound小提琴数字处理器实操指南:从接线到演出的完整配置

电小提琴或者原声小提琴插电演出,第一个绕不开的坎就是声音难听。原声琴的共鸣和空气感一旦进了拾音器,出来的往往是一坨干瘪、发尖、带着奇怪塑料味的信号。我当初第一次把琴接上乐队调音台,直接被主唱吐槽"你这声音像在锯钢丝"。…

2026/9/5 2:46:52

传感器接口IC如何攻克生物化学传感的微弱信号难题?

1. 从电极到比特流:为什么生物化学传感必须依赖专用接口IC 做生物化学传感的人都有过类似的经历:明明传感器本身性能很好,信号输出却一塌糊涂——噪声大、漂移明显、重复性差,怎么调都达不到预期。很多时候问题并不在传感器&#…

2026/9/5 2:44:34

STM32F411CEU6多通道ADC采集:扫描模式+DMA实现详解

1. 多通道 ADC 的用武之地把“Multichannel ADC”和“STM32F411CEU6”这两个关键字放在一起,其实就是嵌入式开发里最常遇到的一类需求:用一块不算贵的 MCU,同时采集多路模拟信号。STM32F411CEU6 是 48 引脚的 Cortex-M4F 主控,主频…

2026/9/5 0:04:47

流式背压机制:避免前端渲染卡死与内存暴涨的滑动窗口限流

流式背压机制:避免前端渲染卡死与内存暴涨的滑动窗口限流在大模型流式输出(Streaming)与智能体实时推流的架构中,生产环境中经常出现一种“上下游生产消费速率严重失衡”的极端情况: 生产端极速产出:大模型…

2026/9/5 2:45:13

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/5 2:30:42

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/5 2:46:50

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…