【JUC 进阶实战】别死磕 synchronized 了!搞懂 ReentrantLock 与排查“死锁”,才算真正懂并发
在上一篇博客中,我们扒开了对象头,看透了 synchronized 这把隐形的锁。在简单的场景下,它确实好用。但在真实的后端高并发开发中,我们经常会遇到一些让人抓狂的需求:
-
“老板,这个线程等锁等了 10 秒了,能不能让它别等了,直接返回失败?”
-
“能不能只唤醒那个负责写数据库的线程,别把所有线程都叫醒?”
面对这些需求,死板的 synchronized 只能摊摊手表示无能为力。今天,我们就来解锁 Java 并发包(JUC)里的高级兵器库,彻底掌控线程的生杀大权!
一、 精准狙击手:LockSupport (park / unpark)
如果你用过原生的 Object.wait() 和 notify(),你肯定体会过那种“盲人摸象”的痛苦:你只能通过 notifyAll() 把所有等待的线程全叫醒,根本无法精准控制。
1. 是什么?(概念与作用)
LockSupport 就像是工厂里的**“精准调度员”**。它不需要配合任何锁(Monitor)使用,而是直接作用于具体的某一个线程。
-
特点 1:无门槛。不需要像
wait/notify那样必须写在synchronized代码块里。 -
特点 2:精准打击。你可以明确指名道姓地暂停或唤醒某一个线程(比如
unpark(线程A))。 -
特点 3:允许“先发车票,再上车”。哪怕你先调用了唤醒(
unpark),再去调用暂停(park),线程也不会被阻塞!
2. 怎么用?(代码示例)
Java
import java.util.concurrent.locks.LockSupport;
public class LockSupportDemo {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
try { Thread.sleep(1000); } catch (Exception e) {} // 故意磨蹭1秒
System.out.println("打工人:准备调用 park() 休息...");
// 因为老板提前发了凭证,这里根本不会休眠,直接放行!
LockSupport.park();
System.out.println("打工人:手里有凭证,继续干活!");
});
worker.start();
// 老板(主线程)动作极快,在员工 park 之前,就提前发了凭证
System.out.println("老板:提前给打工人发通行证 (unpark)");
LockSupport.unpark(worker);
}
}
/* 输出结果:
老板:提前给打工人发通行证 (unpark)
打工人:准备调用 park() 休息...
打工人:手里有凭证,继续干活!
*/
3. 重点细节:底层的秘密(cond / counter / mutex)
为什么 LockSupport 能做到“先发车票后上车”?因为它的底层(C++ 实现)维护了一个包含三个关键属性的机制:
-
_counter(凭证): 这就是那张车票,它的值最多只能是 1,最少是 0。unpark会把它变成 1,park会检查它是不是 1,如果是,就消耗它(变成 0)并放行。 -
_mutex(互斥量)&_cond(条件变量): 当_counter为 0 时,底层会利用操作系统级别的mutex和cond让线程真正进入挂起休眠状态。
二、 锁的暗黑陷阱:活跃性问题(死锁 / 活锁 / 饥饿)
锁用得好是保护伞,用不好就是定时炸弹。在并发编程中,有三种让程序“生不如死”的状态。
1. 死锁 (Deadlock):互不相让的僵局
-
场景: 张三拿着金钥匙,想要银钥匙;李四拿着银钥匙,想要金钥匙。两人互相死等,永远卡住。
-
排查神技(面试必考): 线上环境发生死锁,千万别盲猜!直接用 JDK 自带的工具:
-
敲入
jps命令,找出当前运行的 Java 进程号(PID)。 -
敲入
jstack <PID>,查看线程堆栈信息。如果是死锁,JVM 会在日志最后极其贴心地打印出:"Found one Java-level deadlock!"(找到一个死锁),并列出是哪两个线程在互相卡脖子。或者直接使用图形化界面jconsole也能一键检测死锁。
-
-
破局方案: 保持顺序加锁。让张三和李四都必须先拿金钥匙,再拿银钥匙,僵局瞬间瓦解。
2. 活锁 (Livelock):谦让过头的尴尬
-
场景: 两人在狭窄的走廊迎面碰上。为了让路,两人同时往左闪,又同时往右闪,一直在动,但就是谁也过不去。
-
破局方案: 增加随机的睡眠时间。网络底层协议(如 CSMA/CD)处理冲突就是用的这招:撞车后,各自随机睡 1~5 毫秒再试,完美错开。
3. 饥饿 (Starvation):被遗忘的角落
-
场景: 某个线程优先级太低,或者其他线程太霸道,导致它永远抢不到 CPU 执行权,活活饿死。
三、 终极兵器:ReentrantLock(可重入锁)
为了解决 synchronized 的死板,JUC 提供了 ReentrantLock。它不仅和 synchronized 一样是可重入的(自己加的锁,自己内部再调用其他加锁方法不会卡死自己),还多了四大超能力!
1. 是什么?(四大超能力)
-
可中断(解决死等): 线程在排队等锁的时候,可以被别人用
interrupt()打断,直接抛出异常走人,不再死等。 -
可设置超时(解决死锁):
tryLock(1, TimeUnit.SECONDS)。我就等 1 秒钟,拿到就干活,拿不到我就去干别的,绝不一根筋死磕。 -
可设置公平锁(解决饥饿):
new ReentrantLock(true)。开启公平模式,所有线程必须乖乖排队,先来后到,杜绝有人插队导致老实人饿死。 -
多条件变量(解决盲目唤醒): 可以创建多个
Condition(WaitSet 等待室)。比如,你可以把所有等待写数据库的线程关进ConditionA,读数据库的关进ConditionB。唤醒时,可以精准只唤醒 A 里的线程(conditionA.signal())。
2. 怎么用?(基本语法与避坑)
⚠️ 绝对铁律: 使用 ReentrantLock,必须、一定、千万要在 finally 块中手动释放锁 unlock()!否则一旦业务代码抛出异常,这把锁将永远无法释放,引发灾难!
Java
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
// 实例化一把普通(非公平)的可重入锁
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
// 超能力演示:尝试获取锁,最多等 2 秒
boolean isLocked = false;
try {
System.out.println("t1:我尝试去拿锁...");
isLocked = lock.tryLock(2, TimeUnit.SECONDS);
if (isLocked) {
System.out.println("t1:拿到锁了!开始处理核心业务(临界区)...");
} else {
System.out.println("t1:等了 2 秒都没拿到,不等了,去执行备用逻辑!");
}
} catch (InterruptedException e) {
System.out.println("t1:排队期间被打断了!");
} finally {
// ⚠️ 重点细节:只有真正拿到了锁,才需要释放锁!
if (isLocked) {
lock.unlock();
System.out.println("t1:业务处理完毕,释放锁。");
}
}
});
// 主线程先抢占锁,死死抱住不放
lock.lock();
System.out.println("主线程:我先霸占这把锁 5 秒钟!");
t1.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {} finally {
lock.unlock();
}
}
}
/* 输出结果:
主线程:我先霸占这把锁 5 秒钟!
t1:我尝试去拿锁...
t1:等了 2 秒都没拿到,不等了,去执行备用逻辑!
*/
四、 总结与对比
最后,我们用一张表来总结今天的主角 ReentrantLock 与老牌 synchronized 的对决:
| 特性 | synchronized (关键字) | ReentrantLock (JUC 类) |
| 底层实现 | JVM 层面 (C++ Monitor) | API 层面 (Java 代码,基于 AQS) |
| 锁的释放 | 发生异常或执行完毕,自动释放 | 必须手动在 finally 中调用 unlock() |
| 灵活性 | 只能死等,不可中断 | 支持超时获取 tryLock、可中断 |
| 公平性 | 绝对的非公平锁 | 默认非公平,可传 true 开启公平锁 |
| 精准唤醒 | 只有一个 WaitSet,notifyAll 唤醒所有 |
可配置多个 Condition,精准唤醒 |
建议: 如果业务场景简单,优先使用 synchronized,因为代码简洁,且 JDK 团队一直在对其底层做优化(如锁升级)。但如果你的业务面临复杂的并发场景(如需要超时防止死锁、需要按条件精准唤醒),别犹豫,直接上 ReentrantLock!
更多推荐

所有评论(0)