Java多线程调试需结合线程状态、锁持有和调度时序,用jstack查卡点、IDE线程级断点单步、jcmd+JFR验锁释放,Spring定时任务须配多线程池防阻塞。

Java多线程调试不能靠 System.out.println 硬扛,必须结合线程状态、锁持有、调度时序三方面定位问题。
怎么看线程当前在哪儿卡住?用 jstack 抓现场快照
死锁、无限等待、CPU空转时,jstack 是第一响应手段。它输出所有线程的栈帧、锁状态和阻塞原因。
- 重点关注
WAITING (on object monitor)和BLOCKED (on object monitor)状态,它们后面通常跟着锁对象地址(如0x0000000712345678) - 搜索
java.lang.Thread.State: BLOCKED,看谁在等哪把锁;再搜同一锁地址,找到持有者线程及其栈 - 若发现两个线程互相等待对方持有的锁,就是典型死锁——
jstack末尾会单独标出Found 1 deadlock. - 避免在生产环境频繁执行,每次调用会触发全局 safepoint,可能暂停所有应用线程
怎么在 IDE 里单步跟踪多个线程的执行?启用线程级断点
IntelliJ IDEA 和 Eclipse 都支持线程感知调试,但默认行为容易误导:断点一停,所有线程都挂起。
- 右键断点 → More... → 勾选
Suspend: Thread(不是All),这样只暂停当前线程,其他线程继续跑 - 在断点处打开
Debug工具窗口,切换到Threads标签页,可手动 resume/suspend 任意线程 - 对
synchronized块或ReentrantLock.lock()设置方法断点时,注意 JDK 内部实现可能跳转到Unsafe.park,建议直接在业务代码行设断点 - 不要在
wait()或await()调用行设“线程挂起”断点,否则无法唤醒,调试器本身会卡住
怎么确认锁是否被正确释放?用 jcmd + JFR 查锁生命周期
仅靠日志或断点很难验证 unlock() 是否真被执行,尤其在异常分支中遗漏 finally。
立即学习“Java免费学习笔记(深入)”;
- 启动 JVM 时加参数:
-XX:+UnlockDiagnosticVMOptions -XX:+LogVMOutput -Xlog:monitoring=debug(JDK 10+)可记录锁获取/释放事件 - 更轻量的方式是用
jcmd观察VM.native_memory summary Internal区内存变化,长期增长可能暗示锁未释放导致元数据泄漏 - 对关键锁操作开启 Java Flight Recorder:
-XX:+FlightRecorder -XX:StartFlightRecording=duration=60s,name=lockcheck,settings=profile,录制后用 JDK Mission Control 查看jdk.JavaMonitorEnter和jdk.JavaMonitorExit事件配对情况 -
synchronized的退出是 JVM 自动保证的,但ReentrantLock必须显式unlock(),且不能在不同线程 unlock —— 这类错误不会抛异常,只会导致后续线程永久阻塞
为什么加了 @Scheduled 却不执行?检查 Spring 的线程池配置
Spring 的定时任务默认使用单线程的 ThreadPoolTaskScheduler,一旦某个任务阻塞或抛未捕获异常,后续所有定时任务都会积压。
- 查看日志是否有
TaskScheduler相关 warn,例如Failed to complete scheduled task或Executor thread pool is exhausted - 在配置类中显式定义
TaskSchedulerBean,并设置核心线程数与队列容量:@Bean public TaskScheduler taskScheduler() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(5); scheduler.setThreadNamePrefix("scheduled-"); scheduler.setWaitForTasksToCompleteOnShutdown(true); return scheduler; } - 用
@Async方法调用定时任务逻辑时,务必确保该方法所在类被 Spring 管理(非new实例),否则代理失效,异步不生效 - 若使用
SimpleTrigger或CronTrigger,注意startDelay和initialDelay单位是毫秒,写成10就是 10ms,不是 10s
多线程调试最易忽略的是「时间窗口」—— 问题只在高并发、特定调度顺序、GC 暂停后才复现。本地单步永远看不到真实竞争态,必须结合 jstack 快照、JFR 录制和生产日志中的时间戳交叉比对。











