资讯动态

从线程状态到jstack:Java线程生命周期实战排查指南

发布时间:2026/10/18 8:55:41 来源:尧图企业网站定制
做并发编程的这几年来我几乎每月都要和线程状态打交道。“Java线程生命周期”这个看似基础的概念却总能在线上出问题时让很多人栽跟头。我也见过不少同学能背出六种状态的名字但一看到 jstack 的 dump 输出就懵了分不清哪些是健康的等待、哪些是异常卡顿的信号。这篇文章我就从从业者的实际经验出发把线程生命周期从理论到实战完整拆解一遍希望能帮刚开始学并发的新手建立正确框架也帮工作两三年的朋友补上一些平时容易忽略的细节。1. 六种状态全览先搞清楚每个状态到底是什么1.1 一张类比图记住六种状态Thread.State 枚举里一共有六个值NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。我先打个比方把线程当成银行里处理业务的柜员或客户这套状态就好理解多了NEW叫号系统里刚取出来的号还没分配给任何柜台也就是线程对象已经创建但还没调用 start()。RUNNABLE号已经在等待区排队或者正在某个窗口办理业务。对应到 Java 里就是线程可以被调度器执行或者正在执行。BLOCKED你拿着号想去某个柜台但那个柜台的门口挡着一条隔离带你得等前面的人办完才能进去。对应的是想进入 synchronized 代码块但锁被别人持有。WAITING你把资料递进窗口工作人员说“需要后台审核你等着”但没告诉你具体什么时候好。对应 wait()、join()、LockSupport.park() 这些不带超时参数的等待。TIMED_WAITING工作人员说“你去外面等最多等到下午三点”。对应 sleep(毫秒)、wait(毫秒)、join(毫秒)、parkNanos()、parkUntil() 这些带明确时间的等待。TERMINATED业务办完号作废人离开了。线程 run() 方法执行完毕或者抛出异常终止。这个类比虽然不完全严谨但对入门来说足够直观。真正到了源码层面就是一段简短的枚举声明public enum State { NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED; }1.2 状态之间的流转主干我梳理了这些年最常用到的状态迁移路径它们几乎覆盖了所有标准场景NEW → RUNNABLE调用 start() 之后线程才真正进入可运行状态。RUNNABLE → WAITING调用无参的 wait()或者 join()或者 LockSupport.park()。RUNNABLE → TIMED_WAITING调用 sleep(ms)、带超时的 wait(ms)、join(ms)、parkNanos()、parkUntil()。RUNNABLE → BLOCKED尝试进入 synchronized 方法或块但锁被别人持有。BLOCKED → RUNNABLE成功获得 synchronized 锁。RUNNABLE → TERMINATEDrun() 正常结束或者抛出未捕获异常。有几个易错点我需要特别强调等待 synchronized 锁时是 BLOCKED而等待 ReentrantLock 等基于 AbstractQueuedSynchronizer 的锁时底层调用 LockSupport.park()线程状态是 WAITING不是 BLOCKED。这两个状态的区别经常被误解。另外一旦线程启动就没有任何路径能回到 NEWTERMINATED 是终态不可能被重新启动。我用了一个很容易记的口诀带时间参数的等待进 TIMED_WAITING涉及锁竞争的阻塞进 BLOCKED其余无期限的阻塞进 WAITING。1.3 为什么 RUNNABLE 其实是个“粗粒度”状态初学者往往会有一个疑问一个正在激烈运算的线程和一个被操作系统暂停等待 CPU 调度的时间片线程状态为什么都是 RUNNABLE这是 Java 的设计取舍。JVM 没有直接把操作系统里的“运行”和“就绪”拆成两个状态而是合并成了一个 RUNNABLE。放到操作系统的视角看RUNNABLE 可能对应“正在 CPU 上执行”也可能对应“排在就绪队列里等待调度器分配时间片”。如果你的程序跑在 Linux 上一个线程在做阻塞式 Socket 读状态也经常显示为 RUNNABLE因为 JVM 把 I/O 等待也算进了可运行范畴。这个细节会在后面讲 jstack 和性能排查时反复遇到先有个印象。2. 状态转换的触发场景与必要条件2.1 NEW 到 RUNNABLEstart() 的一小步JVM 的一大步我见过一个很典型的新手错误创建了 Thread 对象后直接调用 run()发现程序也能跑于是以为线程“启动”了。其实这不叫启动线程而是普通对象的方法调用整个过程仍然在调用线程里执行。只有调用 start()JVM 才会通过 native 方法 start0() 向底层操作系统申请创建新的线程由 JVM 的线程调度器接管随后线程状态从 NEW 转为 RUNNABLE。start() 方法还有个“一次性”约束同一线程对象不能重复调用 start()第二次会抛出 IllegalThreadStateException。背后的原因不复杂线程对象第一次启动后底层线程资源已经创建并绑定第二次启动既没有意义也违反了 JVM 的线程管理规则。这里要补充一个重要细节什么情况下线程对象是 NEW 状态但底层线程还没创建答案是所有情况下都是如此。NEW 状态意味着 Thread 对象已经 new 出来了但还没有和任何操作系统线程关联。在线程池场景里核心线程是惰性创建的第一次提交任务时才真正执行 start()所以线程池初始状态下“线程数”未必等于“活跃线程数”这个特性和状态理解直接相关。2.2 BLOCKED 与 WAITING两个容易混淆的阻塞态BLOCKED 和 WAITING 是日常排查中“出现率”最高的两种阻塞状态我花点篇幅把它们彻底讲清楚。BLOCKED 只和 synchronized 关键字有关。线程进入 synchronized 方法或代码块时需要持有对应对象的监视器锁。如果锁已经被其他线程持有当前线程就被阻塞进入 BLOCKED 状态直到锁被释放后重新竞争成功。注意BLOCKED 状态的线程不会响应 interrupt() 中断请求它只能等到锁释放后再次尝试获取锁。WAITING 则是一大类无期限等待的统称常见触发方式有三种Object.wait()必须持有锁才能调用进入等待后当前线程会释放锁直到其他线程调用 notify() / notifyAll() 才可能唤醒。Thread.join()当前线程等待目标线程执行结束。LockSupport.park()当前线程被阻塞挂起需要其他线程调用 unpark() 或发生中断才能唤醒。ReentrantLock 获取不到锁时最终就是通过 LockSupport.park() 挂起所以线程 dump 中经常看到 WAITING (parking) 状态伴随 AQS 栈信息。这是一个高频出现的关键线索在线程池和 JUC 锁相关的排查中几乎天天见。记住一句话synchronized 算阻塞走 BLOCKEDJUC 框架里的等待主要走 WAITINGparking。2.3 TIMED_WAITING 的多个入口以及“带着锁睡觉”的陷阱TIMED_WAITING 比 WAITING 多了一个时间维度。常见入口包括Thread.sleep(ms)休眠指定时间不释放任何锁。Object.wait(ms)带超时释放锁等待被唤醒或超时。Thread.join(ms)带超时等待目标线程结束。LockSupport.parkNanos(nanos) / LockSupport.parkUntil(deadline)。Condition.await(ms) 系列方法。我踩过最深的坑就是“带着锁 sleep”。某个模拟项目里我在 synchronized 方法中调用了 Thread.sleep(500) 模拟耗时结果多个线程全部挤到一个锁上后面的线程要么处于 BLOCKED要么 TIMED_WAITING。当时从 dump 里看到大量 TIMED_WAITING 直觉以为是 wait 调用导致的仔细看栈信息才发现是 sleep 卡在锁内。这给我的教训是状态名称只能说明“线程在做什么类型的等待”不能说明“这个等待是否合理”必须结合调用栈和业务场景一起判断。3. 用 jstack 实战解读线程状态3.1 生成一份线程 dump 的正确姿势排查线程问题时jstack 是最直接的工具。基本流程如下# 先找到 Java 进程 PID jps -l # 打印线程 dump输出到文件避免字符乱码和滚动丢失 jstack -l pid thread_dump_1.txt # 等 5 到 10 秒再打一份 jstack -l pid thread_dump_2.txt如果 JDK 版本较新也可以用 jcmdjcmd pid Thread.print thread_dump_1.txt在生产环境操作时需要注意几点打 dump 本身会短暂暂停目标 JVM所以不要在高峰期高频执行间隔几秒足够不用连续狂打。一份 dump 只能看到瞬时状态我建议至少间隔 3 到 5 秒打两份以上对比同一线程的状态变化来判断线程是否在推进。如果现场不方便用命令行工具可以在代码里用 Thread.getAllStackTraces() 定期输出结合日志记录关键节点的线程状态。这种情况最常见于业务无法随意触发 dump 的场景相当于在应用内部埋了一个观测口。3.2 从 dump 输出中快速定位问题的套路先说一个关键认知不是在 dump 里看到 WAITING 就一定是故障。线程池里的空闲线程常态就是 WAITING (parking)。真正需要警惕的是大量线程同时停留在某个“不正常的栈位置”。举一个典型例子某次线上批量任务突然卡住我用 jstack 打了两个时点的 dump。核心栈信息如下pool-4-thread-3 prio5 tid0x00007f1d8c166800 nid0x4a3b waiting for monitor entry java.lang.Thread.State: BLOCKED (on object monitor) at com.example.demo.BatchWorker.run(BatchWorker.java:88) - waiting to lock 0x00000000d6b1a220 (a com.example.demo.SharedResource) at com.example.demo.BatchWorker.run(BatchWorker.java:95)另一个线程的栈信息指向同一个锁对象并且持有锁后正在执行耗时操作。这种“两个线程同时出现在同一个锁对象上一个持有一个等待”的模式几乎可以立刻断定是锁竞争导致的任务堆积。再看另一个高频情况java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) at java.util.concurrent.locks.LockSupport.park(Unknown Source) at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(Unknown Source) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueuedInterruptibly(Unknown Source) at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(Unknown Source)如果大量业务线程都卡在 ReentrantLock 的获取逻辑上说明锁的竞争烈度很高要么持锁时间过长要么锁粒度太大。结合我刚才说的“两份 dump 对比”判断持有锁的线程是否长时间不释放是确定问题根因的关键一步。3.3 一个速查表常见状态到底是不是问题我整理了一个速查表方便日常工作快速对照线程状态常见业务含义是否通常属于异常TIMED_WAITING (sleeping)定时任务休眠、限流等待、主动 sleep 模拟耗时数量少正常数量多需结合场景TIMED_WAITING (on object monitor)带超时的 wait()等待条件满足或超时如果长时间不超时需要关注TIMED_WAITING (parking)带超时获取信号量、条件队列等待视业务而定WAITING (parking)线程池空闲核心线程挂起、ReentrantLock 等待锁、条件队列等待线程池场景正常锁等待场景需注意WAITING (on object monitor)无超时 wait()等待其他线程 notify多任务协作中正常单独大量出现有风险BLOCKED (on object monitor)被 synchronized 锁阻塞数量多或持续不变基本有锁竞争问题RUNNABLE正在执行、等待 CPU、或在做系统 I/O需结合 CPU 看是 CPU 密集还是 IO 等待这个表的价值在于提醒一个点状态只是表象真正有价值的永远是“数量 持续时间 可视化调用栈”三个信息合在一起。3.4 状态持续 RUNNABLE 但 CPU 不高是怎么回事前面提过 Java 的 RUNNABLE 比较粗粒度在 Linux 环境下尤其需要注意。如果 jstack 里大量线程都显示 RUNNABLE但系统整体 CPU 使用率并不高这种情况下通常不是线程在密集计算而是这些线程在做某些底层 I/O 等待。典型的例子是阻塞式 Socket 读取、等待数据库连接返回、读取本地大文件等。我在排查一个慢接口时dump 里看到几乎所有工作线程都是 RUNNABLE栈信息停在 SocketInputStream.read 上。当时第一反应以为是 CPU 资源不够但系统负载明明很低后来才意识到是下游服务响应慢线程全都堵在 I/O 读取上而 JVM 依然把这些线程标成了 RUNNABLE。这个场景用 jstack 很难直接判定往往需要结合业务超时时间、下游耗时、网络连接池等上下文信息一起分析。4. 生命周期管理的实战误区与经验4.1 sleep 与 wait看起来都是等着本质完全不同这组对比是并发编程里最经典的考查点也是实际编码里最容易写错的场景。sleep 是 Thread 的静态方法“让自己休息一会儿”它不会释放任何锁所以如果线程在 synchronized 方法里 sleep其他线程依然无法进入这个锁保护的区域。wait 是 Object 的方法“把锁交出去等别人叫我”它的前提是必须持有该对象的监视器锁调用后监事锁立即释放直到被 notify / notifyAll 唤醒或超时。用一句话总结sleep 是自己睡自己的和锁无关wait 是释放锁资源再等待通知本质是一种线程协作手段。日常写代码时模拟耗时用 sleep 没问题但多个线程之间做协作、等待某个条件满足时优先考虑 wait / notify 或 JUC 里对应的 Condition 工具。我自己在项目里见过一种写法某同学想“让当前线程等一会儿再重试”直接用了 wait(1000)结果因为没有持有锁程序抛了 IllegalMonitorStateException。这个异常很多人见第一眼很懵原因就是没理解 wait 的锁前置条件。4.2 中断对线程状态的影响以及正确的清理姿势调用 interrupt() 并不会直接把线程状态改成某个特殊枚举它的作用更像“给线程设置了一个中断标志”。只有当线程进入可中断的阻塞等待时这个标志才会被操作系统或 JVM 感知然后抛出 InterruptedException并清除中断标记。可中断的等待包括Thread.sleep()Object.wait() / wait(ms)Thread.join() / join(ms)LockSupport.park() 系列Condition.await() 系列这些方法都有共同特征它们的签名里声明了 throws InterruptedException。相关线程在等待状态中收到中断信号就会立刻从阻塞中醒来然后抛异常。对于不可中断的 synchronized 阻塞BLOCKED 状态interrupt() 只能设置标志位线程依然卡在锁等待上所以如果想靠 interrupt 唤醒一个正被 synchronized 阻塞的线程在多数场景下是无效的。一旦捕获了 InterruptedException千万不要吞掉这个中断异常。至少有三种正确的处理姿势如果当前方法可以上抛异常就直接声明 throws InterruptedException 交出去。不能上抛时调用 Thread.currentThread().interrupt() 把中断标志恢复回去让上层逻辑继续感知。明确当前不关心中断则至少加注释说明为什么忽略而不是无声无息空 catch。我手写过一个线程池关闭工具给线程池里的 worker 发 interrupt让它们从无期限的 take() 中醒来进而优雅退出。如果不理解中断和状态之间的关系这个功能就很难设计正确。4.3 线程池视角线程状态只是“某一瞬间的快照”线程池会复用线程一个线程可能在生命周期内多次经历 RUNNABLE → WAITING → RUNNABLE → BLOCKED 等状态变化。所以在排查时某个线程当前是 WAITING 不代表它有问题它可能只是线程池中正在休息的核心线程。判断线程池是否健康要结合另外几个指标一起看活跃线程数是否长期等于最大线程数。任务队列是否持续积压。线程状态是否长时间不变化。我给线上业务加线程池时都会用带名字的 ThreadFactory比如“order-pool-thread-1”这样 jstack 一打出来一眼就知道是哪条业务链路的线程。这个习惯看起来很小但在靠线程堆栈排查各种线上问题的时候价值极大。它解决了一个最常见的麻烦一堆叫 pool-1-thread-2 的线程你根本分不清谁是谁线上又多线程池共存时更是灾难。更进一步可以在线程池提交任务时在子任务里设置一个简单的上下文标识结合 MDC 之类的日志增强组件把日志和线程行为串起来。这样时间线上一条链路对应哪些线程参与过会非常清楚。4.4 守护线程对生命周期的影响守护线程的英文名是 daemonThread.setDaemon(true) 可以在 start() 之前设置。守护线程有一个非常特殊的行为当 JVM 中只剩守护线程时JVM 会直接退出守护线程也随之中断不会继续执行。这个特性意味着如果你把某些关键任务放在守护线程里希望在主线程结束后还能跑完很可能会失望。我在一个模拟项目里用守护线程去刷异步数据结果服务主进程退出后这个线程的任务只执行到一半就被强制终止了。后来把 setDaemon(true) 去掉改由线程池管理问题才解决。反过来说守护线程很适合后台辅助工作比如监控日志、定期清理过期缓存这类不依赖主流程的任务。它们不需要阻塞 JVM 退出生命周期跟随主程序结束即可。5. JVM 与操作系统视角下的状态映射5.1 从 RUNNABLE 到系统级状态的展开理解线程生命周期不能只看 Java 这边的六个枚举。在 Linux 上一个 Java 线程对应着一个原生线程Java 状态和操作系统状态之间存在一套映射关系。操作系统线程大致有运行、可运行、睡眠可中断、睡眠不可中断、停止等几个典型状态。Java 的 RUNNABLE 至少包含了两种系统级状态正在运行以及已就绪但等待 CPU 时间片。这也是为什么一个正处于“等待网络数据”状态的 Java 线程在 Linux 上通过 top 查看时进程状态可能有 S睡眠标志但在 jstack 里却显示 RUNNABLE。因为 JVM 认为这个线程只是暂时让出了 CPU没有进入 Java 级别的“锁等待”或“对象等待”所以不予区分。理解这个粗粒度设计能帮你避开一个常见的性能误判线程 dump 里全是 RUNNABLE 不代表 CPU 应该打满而 CPU 打满的时候线程 dump 里也未必只有 RUNNABLE——比如某个线程持锁后做大量计算其他线程在锁外 BLOCKED这时候 CPU 消耗高、大量线程却停留在 BLOCKED。系统负载与线程状态之间的对应没有绝对简单的公式必须结合业务解析。5.2 从 AQS 与 LockSupport 看状态迁移的底层机制进入 JUC 框架之后线程挂起和唤醒大部分都依赖 LockSupport。LockSupport.park() 底层调用 Unsafe.park()这会让当前线程从 RUNNABLE 进入 Java 状态中的 WAITING。当另一个线程执行 unpark() 时目标线程才有机会恢复运行。ReentrantLock 获取锁的本质是让线程在获取不到锁时通过 AQS 的等待队列挂起。整个流程大致是尝试通过 CAS 设置同步状态成功则直接获得锁。失败后把当前线程封装成等待节点加入 AQS 队列。在 acquireQueued 中自旋或调用 park() 挂起。当持锁线程释放锁时唤醒队列头部线程。所以你在 jstack 里看到 ReentrantLock 相关线程状态是 WAITING (parking)就是这个机制的直接体现。理解了这条链路以后看到 AQS 相关栈信息就不会觉得陌生也知道锁竞争的真正瓶颈往往在持锁临界区的执行时长上而不是挂起机制本身。5.3 synchronized 与 Lock 的状态可观测性差异synchronized 和 ReentrantLock 都能实现互斥但它们在状态表现上很不一样。前面已经说过synchronized 获取不到锁时线程进入 BLOCKED而 ReentrantLock 获取不到锁时线程经常表现为 WAITING (parking)。从排查角度看synchronized 的状态信息更直观它直接告诉你“在等哪把内置锁”而 ReentrantLock 的 WAITING 栈信息则需要多看几行找到 AQS 队列相关的锁对象标识。从功能角度看synchronized 不响应中断而 ReentrantLock 的 lockInterruptibly() 可以响应中断synchronized 是非公平锁ReentrantLock 可以指定公平策略。现代 JDK 对 synchronized 做了大量优化锁膨胀、偏向锁等技术让它性能并不差而且代码更简洁、状态更直观。我的经验是新代码如果只是简单互斥优先考虑 synchronized如果清晰需要超时、可中断或公平性控制再选 ReentrantLock。5.4 基于状态转换思路优化线程利用率线程生命周期不只是测试题或纯理论它可以直接指导并发设计。举个实际例子某个任务需要同时等待两个远程接口返回最简单的写法是一个个地等这样总耗时是两个接口耗时之和。如果改用带超时的异步等待用 CompletableFuture 或者自己维护一个 CountDownLatch就能让当前线程在等待期间进入 TIMED_WAITING而不是干耗着一个接口的返回时间。从状态角度看前者是线程大部分时间在 RUNNABLE 或阻塞 I/O 中度过后者是线程有效进入 TIMED_WAITING等所有任务都完成后一次性被唤醒。虽然两者都不算“错误”但后者的资源利用率明显更高最大等待时间也从“串行之和”变成“并行最大值”。再举一个例子批量数据同步任务如果每个子任务调用一个远程接口且每个远程调用需要 3 秒那么一个线程最多能处理非常有限数量的任务其他任务都在排队。这时候把同步调用改成带超时的并发提交线程会在 TIMED_WAITING 状态等待操作系统调度也能把 CPU 让给更有价值的任务。这是我对线程生命周期最实用的一种理解状态不是给别人看的指标而是直接影响你系统吞吐量的隐形因素。最后再分享一个排查技巧最后分享一个我在实际排查中反复验证过的技巧线上问题定位时不要只看一份 dump一定要持续观察几份 dump 的状态变化。我遇到过一个诡异的现象某服务中间件线程总是处于 BLOCKED单独看每一份 dump 都像是死锁。连续打印了五份 dump 对比之后才发现这个线程每隔几秒是可以获得到锁并执行的只是每次被我们采样的瞬间恰好都卡在锁等待上。如果只看一份 dump很容易误判为“死锁”然后一顿乱操作。所以我在团队里一直强调线程状态是一个“动态快照”单独一个瞬间的 dump 反映信息有限多份 dump 之间对比状态不变才是真正值得怀疑的卡死状态在变说明线程还在推进。这个思路配合线程池队列长度、业务日志耗时一起看大多数线程问题都能在十分钟内定位到根因。线程生命周期这个概念不难但真正的功力在于把六个状态放到真实运行环境里用工具观察、结合业务分析、再回头优化设计。多在自己的项目里试试用 jstack 看状态变化比背十遍枚举定义都有用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑