Chapter 10

并发调试与性能分析

死锁检测、竞争条件、JFR、async-profiler、jstack——让并发问题无所遁形

并发 Bug 的特点

并发 Bug 是软件工程中最难排查的一类问题,主要原因:

死锁检测与分析

死锁的四个必要条件

互斥条件
资源一次只能被一个线程持有(锁的基本属性,通常无法消除)。
占有且等待
线程持有至少一把锁,同时等待获取另一把锁。消除方法:一次性申请所有锁。
不可抢占
锁不能被强制夺走,只能主动释放。消除方法:使用 tryLock 超时,放弃时释放已持锁。
循环等待
线程 A 等 B 的锁,B 等 C 的锁,...,Z 等 A 的锁,形成环。消除方法:固定锁的获取顺序,打破循环。

使用 jstack 检测死锁

// 典型死锁代码
Object lock1 = new Object();
Object lock2 = new Object();

Thread t1 = new Thread(() -> {
    synchronized (lock1) {
        System.out.println("T1 持有 lock1");
        try { Thread.sleep(100); } catch (InterruptedException e) {}
        synchronized (lock2) { // 等待 lock2,但被 T2 持有
            System.out.println("T1 持有 lock1 + lock2");
        }
    }
});

Thread t2 = new Thread(() -> {
    synchronized (lock2) {
        System.out.println("T2 持有 lock2");
        try { Thread.sleep(100); } catch (InterruptedException e) {}
        synchronized (lock1) { // 等待 lock1,但被 T1 持有 → 死锁!
            System.out.println("T2 持有 lock1 + lock2");
        }
    }
});
# 获取进程 PID
jps -l

# 生成线程转储(Thread Dump)
jstack {pid}

# 或通过 kill -3 触发(发送 SIGQUIT)
kill -3 {pid}
# jstack 输出(死锁部分):
Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x000... (object 0x..., a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x000... (object 0x..., a java.lang.Object),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
  at DeadlockDemo.lambda$main$1(DeadlockDemo.java:22)
  - waiting to lock <0x...> (a java.lang.Object)
  - locked <0x...> (a java.lang.Object)
"Thread-0":
  at DeadlockDemo.lambda$main$0(DeadlockDemo.java:12)
  - waiting to lock <0x...> (a java.lang.Object)
  - locked <0x...> (a java.lang.Object)

通过 MXBean 编程检测死锁

import java.lang.management.*;

ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();

// 检测死锁(synchronized 锁)
long[] deadlockedIds = threadBean.findDeadlockedThreads();
// 也检测 java.util.concurrent 锁
long[] allDeadlocked = threadBean.findMonitorDeadlockedThreads();

if (deadlockedIds != null) {
    ThreadInfo[] infos = threadBean.getThreadInfo(deadlockedIds, true, true);
    for (ThreadInfo info : infos) {
        System.err.println("死锁线程:" + info.getThreadName());
        for (StackTraceElement e : info.getStackTrace()) {
            System.err.println("    at " + e);
        }
    }
}

// 定期检测(生产环境监控)
ScheduledExecutorService monitor = Executors.newSingleThreadScheduledExecutor();
monitor.scheduleAtFixedRate(() -> {
    long[] ids = threadBean.findDeadlockedThreads();
    if (ids != null) {
        alertSystem.sendDeadlockAlert(ids);
    }
}, 0, 30, TimeUnit.SECONDS);

竞争条件(Race Condition)排查

常见竞争条件模式

检查-执行(Check-Then-Act)
检查某条件后基于检查结果执行操作,但两步之间条件可能被其他线程修改。典型例子:if (!file.exists()) file.createNewFile()——两步非原子,可能两个线程都看到文件不存在,都执行创建。
读-改-写(Read-Modify-Write)
读取变量、修改、写回,三步非原子。典型例子:i++、count = count + 1。
迭代器失效
遍历集合时另一线程修改,普通集合抛 ConcurrentModificationException,并发集合(如 ConcurrentHashMap)的弱一致性迭代器不保证看到最新修改。

使用 ThreadSanitizer 检测

# Java 生态中的数据竞争检测工具:

# 1. Helgrind(Valgrind 子工具,适合 JNI 代码)
valgrind --tool=helgrind java MyApp

# 2. Java Flight Recorder:记录竞争事件(生产环境友好)
java -XX:StartFlightRecording=settings=profile,filename=app.jfr,duration=60s MyApp

# 3. async-profiler + lock profiling
./profiler.sh -e lock -d 30 -f locks.html {pid}

JFR(Java Flight Recorder)

JFR(Java Flight Recorder)是 JVM 内置的低开销事件记录框架(开销通常 < 1%), 可以在生产环境持续运行,记录 JVM、OS、应用层的关键事件。 Java 11 起 JFR 完全免费开放。

常用 JFR 事件(并发相关)

jdk.ThreadPark
线程被 LockSupport.park() 挂起,记录挂起时长和调用栈。用于分析锁竞争等待时间。
jdk.JavaMonitorWait
线程调用 Object.wait(),记录等待时长和持锁对象。
jdk.JavaMonitorEnter
线程等待进入 synchronized 块(锁竞争),记录等待时长。
jdk.VirtualThreadPinned
虚拟线程被 pin 住(在 synchronized 内阻塞),记录持续时长和位置。
jdk.VirtualThreadSubmitFailed
虚拟线程提交失败(调度器饱和)。
# 启动时录制
java -XX:StartFlightRecording=settings=profile,filename=recording.jfr,duration=60s MyApp

# 运行中动态开启(JDK 工具)
jcmd {pid} JFR.start settings=profile name=myRecording

# 停止并保存
jcmd {pid} JFR.stop name=myRecording filename=/tmp/app.jfr

# 分析:使用 JDK Mission Control(JMC)打开 .jfr 文件
# 查看 "Lock Instances" 视图 → 找出等待时间最长的锁
# 查看 "Thread Dump" → 分析线程状态分布

通过代码配置 JFR

import jdk.jfr.*;

// 自定义 JFR 事件(用于记录业务并发指标)
@Name("com.example.LockWait")
@Label("Lock Wait Event")
@Category("Concurrency")
@StackTrace(true)
public class LockWaitEvent extends Event {
    @Label("Lock Name")
    String lockName;

    @Label("Wait Duration (ms)")
    long waitMs;
}

// 使用
LockWaitEvent event = new LockWaitEvent();
event.begin();
try {
    lock.lock();
    // ... 执行任务
} finally {
    event.lockName = lock.toString();
    event.waitMs = event.getDuration().toMillis();
    event.commit(); // 提交事件到 JFR
    lock.unlock();
}

async-profiler

async-profiler 是最流行的 Java 采样性能分析器,使用 AsyncGetCallTrace 或 perf_events, 不依赖 safepoint,能准确捕获阻塞/等待的代码路径,生成火焰图。

# 安装(macOS/Linux)
curl -L https://github.com/async-profiler/async-profiler/releases/download/v3.0/async-profiler-3.0-linux-x64.tar.gz | tar xz

# CPU 火焰图(找热点函数)
./profiler.sh -e cpu -d 30 -f cpu.html {pid}

# 锁争用分析(找锁等待热点)
./profiler.sh -e lock -d 30 -f lock.html {pid}

# 壁钟时间(包含阻塞时间,适合 I/O 分析)
./profiler.sh -e wall -d 30 -f wall.html {pid}

# 对虚拟线程友好的分析(Java 21+)
./profiler.sh -e wall --thread-filter "VirtualThread" -d 30 -f vt.html {pid}

火焰图解读

X 轴(宽度)
函数占用 CPU 时间/采样数的比例,越宽说明该函数(及其调用链)耗时越多。
Y 轴(高度)
调用栈深度,底部是入口(main),顶部是正在执行的函数。
平顶(Plateau)
顶部很宽的平台,说明该函数本身(不是其调用的函数)占用了大量 CPU,是性能热点。

jstack 和 jcmd 实用命令

# jstack:生成线程转储
jstack {pid}                    # 标准线程转储
jstack -l {pid}                 # 包含锁信息(-l 显示 java.util.concurrent 锁)
jstack -F {pid}                 # 强制转储(线程无响应时)

# 分析线程状态分布(Shell 脚本)
jstack {pid} | grep "java.lang.Thread.State" | sort | uniq -c | sort -rn

# jcmd:全能工具
jcmd {pid} Thread.print         # 等同 jstack
jcmd {pid} GC.heap_info         # 堆信息
jcmd {pid} VM.flags             # JVM 参数
jcmd {pid} VM.native_memory     # 本地内存统计
jcmd {pid} VM.systemproperties  # 系统属性

# 虚拟线程的线程转储(Java 21+)
# 默认 jstack 不显示虚拟线程,用以下方式:
jcmd {pid} Thread.print -e      // -e 展开虚拟线程(JDK 21+)

# 或通过 HTTP(Spring Boot Actuator)
# GET /actuator/threaddump → JSON 格式线程转储(包含虚拟线程)

常见并发陷阱与规避

陷阱一:在 lambda 中捕获可变局部变量

// 错误:编译报错(lambda 要求捕获的变量 effectively final)
int count = 0;
Runnable r = () -> count++; // 编译错误!

// 看似绕过了,实则仍有问题
int[] counter = {0}; // 数组引用 final,但元素可变
Runnable r2 = () -> counter[0]++;  // 编译通过,但多线程下不安全!

// 正确方式
AtomicInteger safeCounter = new AtomicInteger();
Runnable r3 = () -> safeCounter.incrementAndGet(); // 线程安全

陷阱二:线程池中的 ThreadLocal 污染

// 问题:线程池线程被复用,上次请求的 ThreadLocal 值残留
@GetMapping("/api/user")
public User getUser(HttpServletRequest req) {
    CurrentUser.set(req.getHeader("user-id"));
    try {
        return userService.getCurrent();
    } finally {
        CurrentUser.remove(); // 必须!否则下次请求可能读到脏数据
    }
}

陷阱三:错误地使用 volatile

// 错误:以为 volatile 解决了原子性
volatile int count = 0;
void increment() {
    count++; // 非原子!volatile 不保证 i++ 的原子性
}

// 正确
AtomicInteger count = new AtomicInteger();
void increment() { count.incrementAndGet(); }

陷阱四:双重检查的 Map 操作

// 错误:non-atomic compound operation
ConcurrentHashMap<String, List<String>> map = new ConcurrentHashMap<>();
if (!map.containsKey("key")) {   // 检查
    map.put("key", new ArrayList<>()); // 写入(非原子!两步之间可能被插队)
}

// 正确:原子操作
map.computeIfAbsent("key", k -> new ArrayList<>()); // 原子!

陷阱五:过度同步

// 问题:同步整个方法,但只有计数操作需要同步
public synchronized String processLarge(String input) {
    String result = expensiveTransform(input); // 耗时操作,不需要同步
    count++; // 只有这行需要同步
    return result;
}

// 优化:减小同步粒度
public String processLarge(String input) {
    String result = expensiveTransform(input); // 无锁并发执行
    counter.incrementAndGet(); // 原子操作,无需 synchronized
    return result;
}

陷阱六:忽略 InterruptedException

// 错误:吞掉中断信号(线程无法被正常关闭)
void bad() {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        // 什么都不做 —— 吞掉了中断!
        System.err.println("Interrupted");
    }
}

// 正确处理方式一:重新设置中断标志(传递给上层处理)
void good1() {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt(); // 恢复中断标志
    }
}

// 正确处理方式二:向上传播(让调用方处理)
void good2() throws InterruptedException {
    Thread.sleep(1000); // 不捕获,直接抛给调用方
}

并发性能调优检查清单

生产环境并发性能排查步骤

虚拟线程专项调试(Java 21+)

# 检测虚拟线程 pinning(输出 pin 住的堆栈)
java -Djdk.tracePinnedThreads=full -jar app.jar

# JFR 记录虚拟线程事件
java -XX:StartFlightRecording=filename=vt.jfr,settings=profile MyApp
# 在 JMC 中查看:
# - jdk.VirtualThreadPinned:虚拟线程被 pin 的事件
# - jdk.VirtualThreadSubmitFailed:提交失败
# - jdk.VirtualThreadStart/End:虚拟线程生命周期

# 线程数监控(虚拟线程不计入 ThreadMXBean.getThreadCount)
# 使用 jcmd Thread.print 或 /actuator/threaddump

本章小结

全教程总结

至此,你已经完成了 Java 并发编程的完整旅程:从 JMM 内存模型的理论基础,到线程生命周期与 API; 从 synchronized/ReentrantLock 锁机制,到 ConcurrentHashMap/BlockingQueue 并发集合; 从 ThreadPoolExecutor 线程池调优,到 CompletableFuture 异步编程; 从 Java 21 虚拟线程和结构化并发的新特性,到 AtomicXxx/VarHandle 无锁编程; 最后到生产环境的诊断与调优工具。 掌握这些知识,你已具备构建高并发、高可靠 Java 系统的完整能力。