并发问题排查
并发程序的问题,往往难以复现、难以定位、难以修复。
死锁、活锁、竞态条件、内存可见性……每一种都有自己的特点。本文教你用工具和方法,快速定位问题。
并发问题的五种类型
| 问题类型 | 描述 | 典型症状 | 排查难度 |
|---|---|---|---|
| 死锁 | 多个线程相互等待对方持有的锁 | 程序无响应 | 容易 |
| 活锁 | 线程不断重试但无法前进 | CPU 高但无进展 | 中等 |
| 饥饿 | 某些线程长期无法获得资源 | 部分任务永远不执行 | 中等 |
| 竞态条件 | 结果依赖执行时序 | 结果不确定 | 困难 |
| 内存可见性 | 线程间数据不一致 | 读到过期数据 | 困难 |
死锁
死锁演示
java
public class DeadlockDemo {
private static final Object resourceA = new Object();
private static final Object resourceB = new Object();
public static void main(String[] args) {
// 线程1:先 A 后 B
Thread t1 = new Thread(() -> {
synchronized (resourceA) {
System.out.println("线程1: 已获取资源A");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
synchronized (resourceB) {
System.out.println("线程1: 已获取资源B");
}
}
}, "Deadlock-Thread-1");
// 线程2:先 B 后 A(与线程1相反)
Thread t2 = new Thread(() -> {
synchronized (resourceB) {
System.out.println("线程2: 已获取资源B");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
synchronized (resourceA) {
System.out.println("线程2: 已获取资源A");
}
}
}, "Deadlock-Thread-2");
t1.start();
t2.start();
// 等待后检测
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("程序可能发生死锁,使用 jstack 检测");
}
}运行后,程序会卡住。
使用 jstack 检测死锁
bash
# 找到 Java 进程 PID
jps -l
# 导出线程堆栈
jstack <pid> > thread_dump.txt
# 直接查看死锁信息(推荐)
jstack -l <pid>如果存在死锁,jstack -l 会输出:
Found one Java-level deadlock:
=========================
"Deadlock-Thread-2":
waiting for monitor lock ...
java.lang.Thread.State: BLOCKED
"Deadlock-Thread-1":
waiting for monitor lock ...
java.lang.Thread.State: BLOCKED死锁解决方案
方案一:固定加锁顺序
java
// ❌ 危险
void method1() { lock(resourceA); lock(resourceB); }
void method2() { lock(resourceB); lock(resourceA); } // 顺序相反
// ✅ 安全
void method1() { lockA(); lockB(); }
void method2() { lockA(); lockB(); } // 顺序一致方案二:tryLock
java
public class TryLockDemo {
private static final ReentrantLock lockA = new ReentrantLock();
private static final ReentrantLock lockB = new ReentrantLock();
public static void goodMethod() {
while (true) {
if (lockA.tryLock()) {
try {
if (lockB.tryLock()) {
try {
// 操作
return;
} finally {
lockB.unlock();
}
}
} finally {
lockA.unlock();
}
}
Thread.sleep(10); // 避免活锁
}
}
}竞态条件
竞态条件演示
java
public class RaceConditionDemo {
private static int counter = 0;
public static void main(String[] args) throws InterruptedException {
int threadCount = 100;
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter++; // 非原子操作
}
}).start();
}
Thread.sleep(2000);
System.out.println("期望值: " + (threadCount * 1000));
System.out.println("实际值: " + counter);
System.out.println("丢失更新: " + (threadCount * 1000 - counter));
}
}结果不确定,每次运行都不一样。
竞态条件解决
java
public class RaceConditionSolution {
// 方案1:synchronized
private static int counter1 = 0;
private static final Object lock = new Object();
public static void increment1() {
synchronized (lock) {
counter1++;
}
}
// 方案2:AtomicInteger
private static final AtomicInteger counter2 = new AtomicInteger(0);
public static void increment2() {
counter2.incrementAndGet();
}
// 方案3:LongAdder(高并发)
private static final LongAdder counter3 = new LongAdder();
public static void increment3() {
counter3.increment();
}
}活锁
java
public class LivelockDemo {
private static final AtomicBoolean busy = new AtomicBoolean(false);
public static void main(String[] args) {
IntStream.range(0, 2).forEach(i -> {
new Thread(() -> {
while (true) {
if (busy.compareAndSet(false, true)) {
try {
System.out.println("线程" + i + ": 正在工作");
Thread.sleep(100);
} finally {
busy.set(false);
}
}
// 立即重试,没有退让,可能造成活锁
Thread.yield();
}
}, "Livelock-Thread-" + i).start();
});
}
}活锁的解决方案:加入随机退避时间。
内存可见性问题
java
public class VisibilityIssueDemo {
// ❌ 普通变量:可能不可见
private static boolean ready = false;
private static int number = 0;
// ✅ volatile 变量:保证可见性
private static volatile boolean volReady = false;
private static volatile int volNumber = 0;
public static void main(String[] args) throws InterruptedException {
// 内存可见性问题可能表现为:
// Reader 线程永远看不到 Writer 线程的写入
}
}排查工具清单
| 工具 | 用途 | 命令 |
|---|---|---|
| jstack | 线程堆栈 dump | jstack -l <pid> |
| jconsole | 图形化监控线程 | jconsole |
| jvisualvm | 可视化线程分析 | jvisualvm |
| Arthas | 阿里诊断工具 | thread -b 检测死锁 |
| jmc | Java Mission Control | 高级诊断 |
| pidstat | 线程 CPU 使用 | Linux 系统 |
Arthas 常用命令
bash
# 启动 Arthas
java -jar arthas-boot.jar
# 查看所有线程
thread
# 查看线程堆栈
thread <pid>
# 检测死锁
thread -b
# 查看阻塞线程
thread | grep BLOCKED代码级排查清单
java
public class ConcurrencyChecklist {
// 1. 共享可变状态检查
// - 是否有多个线程访问共享变量?
// - 变量是否被修改?
// - 是否有适当的同步?
// 2. 锁顺序检查
// - 获取多个锁时顺序是否一致?
// - 是否可能产生死锁?
// 3. 原子性检查
// - 复合操作是否原子?
// - i++ 等操作是否正确同步?
// 4. 可见性检查
// - 是否有适当的 happens-before 关系?
// - volatile/synchronized 是否正确使用?
// 5. 资源泄漏检查
// - 锁是否在 finally 中释放?
// - 线程池是否正确关闭?
// - ThreadLocal 是否正确清理?
}问题类型与解决方案对照
| 问题 | 排查方法 | 解决方案 |
|---|---|---|
| 死锁 | jstack -l | 固定锁顺序、tryLock、超时锁 |
| 活锁 | 线程 dump | 随机退避、减少重试 |
| 饥饿 | 线程 dump | 公平锁、调整优先级 |
| 竞态条件 | 代码分析 | synchronized、原子类 |
| 可见性 | jconsole/Arthas | volatile、synchronized |
排查最佳实践
- 先复现:在测试环境施加高并发压力,提高复现概率
- 收集证据:jstack、jconsole、Arthas 一起用
- 分析线程状态:BLOCKED、WAITING、TIMED_WAITING 的比例
- 检查锁:jstack -l 专门看锁信息
- 日志辅助:在关键位置添加线程 ID 和时间戳日志
- 代码审查:并发代码 pair review
要点回顾
- 死锁:固定加锁顺序或使用 tryLock
- 活锁:加入随机退避
- 竞态条件:synchronized 或原子类
- 可见性:volatile 或 synchronized
- 工具:jstack、jconsole、Arthas 是排查三件宝
- 预防优于排查:编码阶段就考虑并发安全
