拓冰建站拓冰建站
首页 / 资讯中心 / 正文

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给你更多控制权,也把「记得解锁」的责任交还给了你。按需求选,不按「谁更高级」选。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门