并发问题与排查
约 1101 字大约 4 分钟
布欧-Lewyon
2026-05-16
并发编程中最头疼的不是写加锁代码,而是排查问题。本节覆盖常见的并发问题(死锁、活锁、饥饿)以及生产环境可用的排查手段。
死锁
两个或多个线程互相持有对方所需的锁,永久阻塞。
死锁示例
Object lockA = new Object();
Object lockB = new Object();
// 线程 1
new Thread(() -> {
synchronized (lockA) {
sleep(100);
synchronized (lockB) { System.out.println("T1: 获得两个锁"); }
}
}).start();
// 线程 2
new Thread(() -> {
synchronized (lockB) {
sleep(100);
synchronized (lockA) { System.out.println("T2: 获得两个锁"); }
}
}).start();两个线程永远不会打出日志——它们在互相等待对方释放锁。
排查死锁
# 1. 找到 Java 进程 PID
jps -l
# 2. 打印线程堆栈(含死锁检测)
jstack <PID>jstack 输出中会明确标识:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock <0x...> (a java.lang.Object)
which is held by "Thread-0"
"Thread-0":
waiting to lock <0x...> (a java.lang.Object)
which is held by "Thread-1"预防死锁
- 固定锁顺序:所有线程以相同顺序获取锁。
- 超时尝试:使用
ReentrantLock.tryLock(timeout)替代synchronized。 - 缩小锁范围:只保护必要代码,不跨多个锁嵌套。
// 超时尝试避免死锁
Lock lockA = new ReentrantLock();
Lock lockB = new ReentrantLock();
public boolean transfer() {
if (lockA.tryLock(1, TimeUnit.SECONDS)) {
try {
if (lockB.tryLock(1, TimeUnit.SECONDS)) {
try {
// 临界区
return true;
} finally { lockB.unlock(); }
}
} finally { lockA.unlock(); }
}
return false; // 获取不到则返回失败而非死等
}活锁
线程虽然没有阻塞,但互相让步导致任务无法推进。可以想象两个人迎面相遇,互相让到同一边,又撞上了,再让到另一边……反复没有进展。
// 活锁简化示意
Thread t1 = new Thread(() -> {
while (true) {
if (otherIsActive) {
Thread.yield(); // 让步——两个线程互相 yield,谁都不执行
} else {
doWork();
}
}
});解决方法:增加随机延迟(退避),打破对称让步模式。
饥饿
高优先级线程持续占用资源,低优先级线程长期得不到执行。
// 不公平锁可能导致饥饿
ReentrantLock unfairLock = new ReentrantLock(); // 默认非公平
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
// 使用公平锁降低饥饿风险,但吞吐量会下降常见饥饿场景:
- 线程优先级设置不当(
setPriority在不同操作系统上行为不同,不建议依赖)。 synchronized是非公平锁,高争用下某些线程可能长期获取不到。- 使用
ReentrantLock(true)(公平模式)可缓解,但性能更低。
常见并发 Bug 模式
| Bug 模式 | 表现 | 根因 | 修复 |
|---|---|---|---|
| 丢失更新 | 计数器比预期小 | 复合操作(read-modify-write)无同步 | 用 AtomicInteger 或 synchronized |
| 可见性失效 | 一个线程修改了标志,另一线程看不到 | 缺少 volatile 或同步 | 加 volatile 或用锁 |
| 指令重排序 | 双检锁中拿到未完全构造的对象 | 构造器中的操作被重排序到对象引用赋值之后 | volatile 禁止重排序 |
| 过早通知 | notify() 在条件满足前发出 | 信号量和等待条件不匹配 | 始终在 while 循环中等待 |
使用 jcmd 排查线程问题
# 查看所有 Java 进程
jcmd -l
# 打印线程堆栈(等价于 jstack)
jcmd <PID> Thread.print
# 查看 JVM 运行时信息
jcmd <PID> VM.native_memory summary
# 查看系统属性
jcmd <PID> VM.system_properties
# GC 日志
jcmd <PID> GC.heap_info小结
- 死锁:线程互相等待对方释放锁,
jstack可检测,通过固定锁顺序或tryLock预防。 - 活锁:线程互相让步但无进展,通过随机退避解决。
- 饥饿:某些线程长期得不到资源,公平锁可缓解但性能有代价。
- 常见 Bug:丢失更新(缺同步)、可见性失效(缺 volatile)、指令重排序(缺 volatile 或 final)。
- 排查工具:
jstack(线程堆栈)、jcmd(多功能诊断)、jvisualvm(图形化监控)。 - 易错:死锁不一定每次都会触发——取决于线程调度时序。测试时可能跑 100 次都不触发,上线后在高负载下才出现。所以设计时就要预防,不能只靠测试发现。
- 思考任务:用
jstack分析一个死锁程序,从输出中找出持有锁的线程、等待的线程以及锁对象地址。然后用tryLock改写同一个程序,验证超时机制能避免死锁。
上一节:虚拟线程
下一节:JVM 内存与 GC
