多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Java线程生命周期全解析:状态流转与WAITING/BLOCKED陷阱

Java线程生命周期全解析:状态流转与WAITING/BLOCKED陷阱 上次帮一个朋友排查问题他们线上有个服务偶发卡顿jstack 打出来一看几十个线程全部卡在 WAITING他盯着输出看了半天也不知道问题出在哪。我说你先把状态流转搞清楚那些线程是怎么一步步走进等待的谁唤醒他们谁唤醒不了他们一眼就能定位了。线程生命周期这个知识点说是 Java 并发的基础但在实际工作中真正能讲清楚的人真不多。面试的时候问一句“一个线程从 NEW 到 TERMINATED 要经过哪些状态”能完整答出来的人不到一半更别说把每种状态触发的条件、流转的边界、还有那些容易踩的坑讲明白了。这篇文章就把线程生命周期的状态流转从头到尾拆一遍重点放在状态之间是怎么切换的以及那些“看起来没问题实际写着写着就炸了”的陷阱。无论是面试前突击还是排查线上问题这篇文章都值得通读一遍。1. 为什么线程生命周期值得花一整篇去讲1.1 高频面试考点背后的逻辑线程生命周期几乎是 Java 并发面试的必考题。面试官为什么会反复问这个因为线程状态是理解并发一切机制的基础。你判断一个线程是不是活着用 isAlive()你写 lock 等待唤醒涉及 WAITING 和 BLOCKED你用线程池看的是 worker 线程怎么在 RUNNABLE 和 WAITING 之间反复横跳。一个连线程状态都说不准的人很难让人相信他能写好并发代码。很多人背得出状态名但一问就露馅RUNNABLE 状态下线程一定在执行吗WAITING 和 BLOCKED 到底有什么区别sleep 和 wait 都会让线程停下来这两个状态一样吗如果你也是背完就忘的类型这篇就是给你的——我会把每个状态触发的具体场景、代码写法、jstack 输出长什么样全部展开讲。1.2 线上排查问题必须看懂线程状态线程状态不只是面试题更是排查线上问题的核心工具。服务出问题的时候第一步就是打 thread dump找到线程栈看每个线程卡在什么状态、在哪个代码行。如果连 BLOCKED 和 WAITING 都分不清你看到一屏的“waiting on condition”也只能干瞪眼。举个例子之前有个同事说线程池不够用他看 jstack 发现一堆线程是 WAITING以为是业务线程阻塞了。实际上那些是池子里的空闲 worker正在等队列里的新任务这是完全正常的。真正异常的可能是某一条 BLOCKED说明锁竞争冲突了。这个差异不靠猜靠对线程状态的准确理解。2. 六种状态全解析一张表建立整体认知2.1 Thread.State 枚举的六个状态JDK 的Thread.State枚举里定义了六个状态这是 JVM 视角下的线程状态注意这里说的是 JVM 线程状态不是操作系统线程状态后面我会单独讲两者的差异。先看这六个状态的完整定义。状态含义进入方式退出方式NEW线程已创建但还没启动new Thread() 之后调用 start()RUNNABLE可运行/运行中start() 后、锁竞争成功、被唤醒等进入等待/阻塞/终止BLOCKED阻塞等待监视器锁进入 synchronized 块但锁被占用拿到锁WAITING无限期等待wait() / join() / park()被唤醒 / 等待线程结束TIMED_WAITING有限期等待sleep() / wait(timeout) / join(timeout)超时或被唤醒TERMINATED已终止run() 执行完或抛出未捕获异常无这张表建议直接收藏。对照这张表去理解接下来的每段内容会清晰很多。注意我故意把“进入方式”写成了具体的调用方法因为很多人压根不知道该调用什么方法才会触发状态变化嘴上会背状态名手上一写代码就卡壳。2.2 三个“自然状态”和三个“等待状态”如果进一步归类这六个状态其实可以分成两组。NEW、RUNNABLE、TERMINATED 这是一组它们描述的是线程“活着还是没活、在不在跑”的自然生命状态BLOCKED、WAITING、TIMED_WAITING 是另一组它们描述的是线程“因为什么原因停下来”的等待状态。这种分组思维很重要。你在看线程 dump 的时候先用第一组状态判断这个线程整体处于什么阶段再用第二组状态看它当前卡在哪。比如一个线程池里的线程整体看一直处在 RUNNABLE 阶段它一直在为某个任务服务没有被销毁重建但某个瞬间可能是 WAITING因为它在等队列任务也可能变成 BLOCKED因为它在等一把锁。不会归类的话很容易把“线程状态”和“线程阶段”搞混。2.3 JVM 线程状态和 OS 线程状态不对等这是一个特别容易踩的盲区JVM 的 RUNNABLE 状态其实糅合了操作系统态里的“运行中”和“可运行就绪”两个状态。JVM 在记录线程状态的时候并没有区分线程正在 CPU 上跑还是排队等待 CPU只要线程没有被锁、等待、阻塞这类条件卡住就是 RUNNABLE。这就意味着你用 jstack 看到的 RUNNABLE不代表那个线程真的在消耗 CPU。一个线程如果正在做网络 IO 等待从 JVM 视角看它依然是 RUNNABLE因为 CPU 执行权已经被让出去了但 JVM 没有专门记录“IO 等待”这个状态。遇到过“线程显示 RUNNABLE 但是感觉卡住了”的场景吗很可能就是 IO 阻塞了但状态显示为 RUNNABLE这个点后面实操部分还会回来讲。3. 状态流转的完整路径与非法流转3.1 合法的状态流转路径线程的状态不是随意跳转的。有合法的流转路径你随便写代码也没法让线程从一个状态直接跳到另一个不相关的状态。完整的合法流转路径可以这样梳理NEW - RUNNABLE唯一入口是调用 start() 方法。RUNNABLE - BLOCKED进入 synchronized 同步块/方法但锁已被其他线程持有。BLOCKED - RUNNABLE锁竞争成功拿到监视器锁。RUNNABLE - WAITING调用 Object.wait()、Thread.join()、LockSupport.park()。RUNNABLE - TIMED_WAITING调用 Thread.sleep()、wait(long)、join(long)、parkNanos() 等。WAITING - RUNNABLE被 notify()/notifyAll() 唤醒或者 join() 等待的线程终止或者被 unpark()。TIMED_WAITING - RUNNABLE超时时间到或被唤醒/被打断。RUNNABLE - TERMINATEDrun() 方法正常执行完毕或者抛出未捕获异常。从上面的路径可以看出状态流转的核心枢纽就是 RUNNABLE。等待状态都是从 RUNNABLE 出发的也都会回到 RUNNABLE不会出现 BLOCKED 直接变 WAITING也不会出现 WAITING 直接变 TERMINATED。理解了这个枢纽很多问题就想通了。3.2 三类典型的非法流转陷阱第一类陷阱NEW 状态下直接尝试执行“等待操作”。线程还没 start()你调用它的 wait() 会直接抛 IllegalMonitorStateException因为线程根本没有进入任何等待队列。这已经不是状态流转的问题了而是你根本不应该在一个还没启动的线程上做任何同步操作。第二类陷阱TERMINATED 之后想“复活”。很多新手会以为线程结束之后可以重新 start() 再次执行这是完全错误的理解。一个线程只有一次生命周期一旦进入 TERMINATED再调用 start() 就会抛出 IllegalThreadStateException。你要复用逻辑就重新 new 一个线程或者用线程池。第三类陷阱BLOCKED 和 WAITING 的非法跳转。这两个状态之间的边界经常被人忽略。BLOCKED 是等锁等的是 synchronized 监视器锁WAITING 是等待被明确唤醒比如 wait/park。你不能把一个 WAITING 的线程通过“通知它抢到锁”来唤醒它必须先被 notify然后重新参与锁竞争。写并发代码时如果混用了 synchronized 和 wait/notify 的机制很容易出现线程永远卡住的死锁问题。3.3 状态流转与锁竞争的关系如果想更深入一层可以看看状态流转和锁竞争的关系。synchronized 锁竞争的失败者进入 BLOCKED这是最传统的管程锁。但 JVM 里还有显式锁比如 ReentrantLock它底层用的是 LockSupport.park() 机制线程获取锁失败时进入的是 WAITING而不是 BLOCKED。这就是一个很隐蔽的点同样是等锁用 synchronized 是 BLOCKED用 ReentrantLock 是 WAITING。所以你在 jstack 里看到大量 WAITING 且堆栈指向 AbstractQueuedSynchronizer 的 parkAndCheckInterrupt那基本可以判断是在等显式锁。看到 BLOCKED 且堆栈指向 ObjectMonitor那就等的是 synchronized。这个区分在实际定位锁冲突问题时很有用。4. 实操用代码逼出每一个状态4.1 前提准备给线程起名字别用默认名写演示代码之前先养成一个习惯所有线程创建时给它一个有意义的名字。默认的 Thread-0、Thread-1 在单个线程时还能分清一旦有几十个线程根本没法看。给线程起一个识别度高的名字是在为以后排查问题省时间。Thread thread new Thread(() - { // 业务代码 }, 业务-订单超时扫描线程);这个习惯的价值在调试并发问题时会被无限放大。你 jstack 打出来直接 grep 线程名字一秒定位。配合下面的状态观测方法排查效率最高。4.2 实操代码触发 NEW 到 TERMINATED 的全部状态我直接给出一版演示代码能依次触发六大状态。各位可以自己跑一遍不用额外依赖只要 JDK 8 就能跑。public class ThreadStateDemo { private static final Object LOCK new Object(); public static void main(String[] args) throws Exception { // 1. NEW 状态 Thread newThread new Thread(() - {}, NEW线程); System.out.println(1. NEW - newThread.getState()); // 2. RUNNABLE 状态 Thread runnableThread new Thread(() - { while (true) { // 空循环保持运行 } }, RUNNABLE线程); runnableThread.start(); System.out.println(2. RUNNABLE - runnableThread.getState()); // 3. BLOCKED 状态 Thread blockedThread new Thread(() - { synchronized (LOCK) { // 永远不释放锁 } }, BLOCKED线程); synchronized (LOCK) { blockedThread.start(); Thread.sleep(500); System.out.println(3. BLOCKED - blockedThread.getState()); } // 4. WAITING 状态 Thread waitingThread new Thread(() - { synchronized (LOCK) { try { LOCK.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }, WAITING线程); waitingThread.start(); Thread.sleep(500); System.out.println(4. WAITING - waitingThread.getState()); // 5. TIMED_WAITING 状态 Thread timedWaitingThread new Thread(() - { try { Thread.sleep(5000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }, TIMED_WAITING线程); timedWaitingThread.start(); Thread.sleep(500); System.out.println(5. TIMED_WAITING - timedWaitingThread.getState()); // 6. TERMINATED 状态 Thread terminatedThread new Thread(() - { // 立刻结束 }, TERMINATED线程); terminatedThread.start(); terminatedThread.join(); System.out.println(6. TERMINATED - terminatedThread.getState()); System.exit(0); } }跑一段就能看到控制台输出1. NEW - NEW 2. RUNNABLE - RUNNABLE 3. BLOCKED - BLOCKED 4. WAITING - WAITING 5. TIMED_WAITING - TIMED_WAITING 6. TERMINATED - TERMINATED4.3 用 jstack 观测真实线程栈IDEA 里直接看状态当然没问题但工作中更多时候要看的是生产环境的线程 dump。模拟环境里我们可以用 jstack 自己抓一次# 先找到 java 进程的 PID jps -l # 抓取线程 dump jstack -l PID在 jstack 输出里每一个线程都能看到 nidnative thread id、状态、栈顶调用。比如上面的 BLOCKED 线程输出会指向 synchronized 的某一行WAITING 线程会指向 Object.wait() 那一行。这种“状态 栈顶方法”的组合才是排查问题真正需要的信息。结合前面说的线程名字你在 dump 里 grep 名字就能瞬间定位那个线程到底卡在了哪一行代码。5. 从 NEW 到 RUNNABLE 的启动陷阱5.1 start() 只能调用一次否则直接异常刚才提到过TERMINATED之后不能重新 start()。其实更严谨的说法是一个线程对象从 NEW 状态调用了 start() 之后无论它后来处于什么状态都不能再次调用 start()。哪怕线程还在 RUNNABLE你连续调两次 start()第二次也是直接抛 IllegalThreadStateException。这个机制的底层逻辑很简单每个 Thread 对象对应 JVM 里唯一的一条线程执行历史。start() 会把线程状态从 NEW 改到 RUNNABLE同时向 JVM 申请创建底层线程资源。第二次 start() 检测到线程已经不是 NEW直接拒绝。所以写代码的时候凡是重复启动线程的逻辑基本就是错的该考虑线程池或者重新创建实例。5.2 start() 和 run() 的区别一个经典面试陷阱调用 start() 和直接调用 run() 有什么区别答案是直接调用 run() 根本不会创建新线程run() 会在当前线程同步执行跑完才返回。这就相当于一个普通方法调用整个“并发”的意义完全丢失了。Thread t new Thread(() - System.out.println(Thread.currentThread().getName())); t.run(); // 输出 main没有新线程 t.start(); // 输出 Thread-0有新线程我见过有人把 run() 写在构造器里想“提前启动线程”结果构造器跑完线程早就跑完了完全没有并发的效果。这种错误在代码 review 里格外显眼见到.run()这种调用的先问一句作者是不是漏了一个 start()。5.3 RUNNABLE 状态不等于线程正在工作前面提到 JVM 的 RUNNABLE 把“就绪”和“运行中”合并了。这里要再深入一点谈谈它的实际影响。你在排查问题的时候如果看到一个线程是 RUNNABLE但它的栈顶是 SocketInputStream 的 read 方法这说明它在等待网络数据。从 OS 角度这个线程其实是阻塞在 IO 上的CPU 利用率是 0但 JVM 记录它依然是 RUNNABLE。如果你不了解这一点看到 jstack 里一堆 RUNNABLE 的 IO 等待线程可能会误判成“线程在疯狂跑计算”然后往 CPU 的方向去排查方向就完全偏了。结合 jstack 看RUNNABLE 本身不是结论栈顶才是。先看栈顶方法判断它是在跑计算还是在等 IO再决定排查方向。这个经验比背十个状态定义都有用。6. TERMINATED 生命周期终点的隐蔽细节6.1 run() 抛异常后线程照样终止很多人以为只有 run() 正常返回线程才会终止这个理解是有偏差的。只要 run() 方法抛出未被捕获的异常线程同样会进入 TERMINATED。区别在于抛出未捕获异常时线程会先被Thread.getUncaughtExceptionHandler()处理默认处理逻辑是把堆栈打到 stderr然后终止线程。实际业务里如果线程池中任务执行到一半抛出 RuntimeException线程会终止当前任务但 worker 线程本身会继续存在并等待下一个任务。你在 jstack 里看到的现象就是这个线程状态从 RUNNABLE 或者 TIMED_WAITING 恢复到 RUNNABLE然后继续处理新任务。所以线上排查时不能单靠“线程状态是 RUNNABLE”来判断线程没有异常退出还得配合日志和监控。这里分享一个经验凡是自定义线程都建议设置UncaughtExceptionHandler可以统一记录异常日志还能监控线程异常终止的次数。Thread t new Thread(() - { throw new RuntimeException(boom); }); t.setUncaughtExceptionHandler((thread, throwable) - { System.err.println(线程异常终止: thread.getName() , 异常: throwable.getMessage()); }); t.start();6.2 daemon 线程的“突然消失”一个线程是否是守护线程daemon会直接影响 JVM 的退出行为。当 JVM 中只剩守护线程的时候JVM 会直接退出不管那些守护线程是什么状态。换句话说你一个守护线程正执行到一半如果其他非守护线程都结束了JVM 直接终止你的守护线程连 TERMINATED 状态都来不及走到。这个坑在小程序里不明显在服务端框架中却很常见。比如某些清理线程、监控线程被错误标记为 daemon结果主线程一退清理逻辑直接没了。反过来如果有人在线程池里把 worker 线程设成了 daemon那么主线程退出时整个线程池被强行销毁队列里的任务直接丢失。设置setDaemon(true)之前要想清楚这个线程的生命周期是不是应该跟着主线程走。6.3 join() 与 isAlive() 的判断边界join() 本质上是“当前线程等待目标线程终止”它的实现底层是wait(0)所以 join() 期间当前线程会进入 WAITING。目标线程一旦终止当前线程被唤醒就恢复了 RUNNABLE。这里有个细节join() 只能保证“目标线程终止”不能保证“目标线程的同步块执行完毕”。如果目标线程先执行完它的状态变成 TERMINATED当前线程的 join() 立即返回没有多余等待。isAlive() 也是类似的边界问题。isAlive() 返回 false 的情况有两种还没 start()NEW 状态以及已经 TERMINATED。所以拿 isAlive() 判断线程是否“正在工作”是不严谨的一个还没启动的 NEW 线程 isAlive() 也是 false。必要的时候应该先判断getState() ! NEW再结合 isAlive() 判断。7. 常见混淆点与面试题速查7.1 WAITING 和 BLOCKED 到底怎么区分这是我被问得最多的问题也是面试中最容易答翻车的点。一句话讲清楚BLOCKED 是排队等一把已经被别人持有的 synchronized 锁WAITING 是线程主动放弃了执行权等待一个明确的信号唤醒。用生活化的比喻来说BLOCKED 就像你在超市收银台前排队前面的人挡住了你能做的就是等着不需要额外做任何事WAITING 就像你等人给你打电话你挂了电话放下手里的一切直到对方打过来你才会继续。WAITING 比 BLOCKED 更“消极”因为唤醒它需要的不是抢到锁而是明确的 notify/park 信号。再看一眼特征特征BLOCKEDWAITING触发方式synchronized 锁竞争失败wait() / join() / park()唤醒方式锁持有者释放锁notify() / 线程终止 / unpark()常见堆栈标志ObjectMonitor、synchronizedObject.wait、LockSupport.park是否需要锁才能进入本身就是在等锁有的需要先持有锁wait7.2 Thread.sleep() 和 Object.wait() 的区别这两个方法的区别在面试里出现频率极高也确实能考察对线程生命周期的理解深浅。仔细拆开看区别体现在四个维度状态维度sleep() 进入 TIMED_WAITINGwait(long) 也进入 TIMED_WAITING但 wait() 不传参则是 WAITING。锁维度sleep() 不释放任何锁它只是让线程暂停wait() 会释放掉当前线程持有的监视器锁让其他线程可以进入同步块。唤醒方式sleep() 只能等时间到或被 interrupt()wait() 除了等待超时还可以被 notify()/notifyAll() 提前唤醒。前置条件wait() 必须在 synchronized 块中调用否则抛 IllegalMonitorStateException而 sleep() 没有这个要求。实际使用中很多人把 sleep() 当 wait() 用在同步代码块里 Thread.sleep(1000)结果其他线程进不来整个系统的并发能力被白白浪费了。如果你想让出锁并等待必须用 wait() 而不是 sleep()。7.3 线程池里的线程是什么状态线程池的 worker 线程生命周期跟普通 new 出来的线程不太一样。worker 线程从线程池创建时被启动之后不是跑完一个任务就 TERMINATED而会循环从阻塞队列里取下一个任务。没有任务时它会阻塞在workQueue.take()或者poll(timeout)上对应的状态就是 WAITINGtake或 TIMED_WAITINGpoll 超时。所以看线程池的线程 dump 时看到大量 WAITING 且栈顶是 LinkedBlockingQueue.take这通常是线程池的正常状态而不是业务卡死。如果线程池配置了 allowCoreThreadTimeOut核心线程在空闲超过 keepAliveTime 后才会超时终止状态才会变成 TERMINATED。把线程池行为跟线程状态关联起来看能避免很多误判。7.4 面试题速答清单我把高频考点整理成了一张速查清单考前过一遍可以用最少的时间掌握核心结论问题答案要点线程有哪些状态NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED怎么进入 WAITINGObject.wait()、Thread.join()、LockSupport.park()怎么进入 TIMED_WAITINGThread.sleep()、wait(timeout)、join(timeout)、parkNanos()WAITING 和 BLOCKED 区别前者等通知/等线程结束后者等 synchronized 锁TERMINATED 后还能 start 吗不能抛 IllegalThreadStateExceptionRUNNABLE 状态一定在执行吗不一定可能是就绪或 IO 等待线程异常终止会变 TERMINATED 吗会未捕获异常导致 run() 结束daemon 线程会影响 JVM 退出吗只剩 daemon 线程时 JVM 直接退出8. 排查线程问题的工具链与实战心法8.1 jstack、jcmd、jvisualvm 的组合用法线程状态理论学了再多最终要落到工具之上。最常用的就是 jstack它是 JDK 自带工具直接输出线程 dump。另一个被忽略的利器是jcmd它的Thread.print功能比 jstack 略强还能顺带打印死锁检测信息jcmd PID Thread.print jcmd PID Thread.print -l如果你的系统是 JDK 9jcmd 的 Thread.print 会自动输出死锁检测结果这一点在定位线程互相等待时非常有用。jvisualvm 则是图形化界面适合本地开发环境观察能实时看到线程状态变化的曲线演示教学时对比效果很好。8.2 从线程 dump 反推业务逻辑的方法拿到线程 dump 之后不要一头扎进状态字段里应该按这个顺序逐步排查第一步先看线程名字。线程名如果能区分业务场景直接过滤出自己关心的那批线程。第二步看状态。排除正常的 pool 空闲等待聚焦到 BLOCKED 和异常 WAITING 上。第三步看栈顶。栈顶能直接定位到代码行锁竞争会指向 synchronized 块wait 会指向 wait() 那一行。第四步结合业务日志。线程 dump 只反映快照必须配合日志看这个线程在这个栈顶等了多久。有一次排查服务变慢jstack 里发现两个线程互为 BLOCKED一个持有锁 A 想拿锁 B另一个持有锁 B 想拿锁 A。这种典型的死锁场景jcmd 检查能直接报出来。真正的难点反而不是死锁检测而是在一堆日志里定位到这两条线程对应的业务链路。线程名字起得好这一步能节省你好几个小时。8.3 线上监控线程状态的正确姿势线上环境不应该等着出问题才去抓 jstack。比较成熟的做法是在监控指标里定期采集各线程池的活跃线程数、阻塞线程数以及关键线程的实时状态对可疑线程定时抓取线程栈快照。Java 层可以用 ManagementFactory 的 ThreadMXBean 做定期采集代码如下ThreadMXBean threadMXBean ManagementFactory.getThreadMXBean(); long[] ids threadMXBean.getAllThreadIds(); for (long id : ids) { ThreadInfo info threadMXBean.getThreadInfo(id, 100); System.out.println(info.getThreadName() - info.getThreadState()); }配合定时任务把线程状态以日志或者监控指标的形式记录下来。当时出现卡顿翻历史曲线就能看出线程是在某个时间点开始进入 BLOCKED 状态的再对照当时的发布记录和流量变化就能快速锁定引入问题的版本。这种思路比临时抓 dump 更从容也更靠近“预防而非事后救火”的运维理念。我自己在实际操作里的一个习惯是所有关键业务线程名字上必须带模块标识线程池必须有自定义的 ThreadFactory。线程 dump 一打出来按名字分组按状态排序先处理掉 BLOCKED再观察 WAITING剩下的 RUNNABLE 看一眼栈顶。这套流程跑熟了之后再奇怪的并发问题也能一步步拆出原因来。文章写到这儿线程生命周期这件事基本讲透了。从六种状态的记忆方法到状态流转的合法路径和非法陷阱再到用代码亲手触发每个状态、用工具观测线上问题最后落到面试题速答和排查心法。线程生命周期不是一个孤立的考点它是你理解整个 Java 并发体系的钥匙。把这把钥匙攥在手里后面学线程池、锁、并发工具类都会顺很多。
返回列表