☰
Java线程状态全解析:从面试答案到线上排查工具
2026/10/10 21:50:57 网站建设 项目流程

搞Java并发编程,如果只能记住一个概念,我会选线程状态。面试被问“线程有哪几种状态”,背出答案不难,但真正能把状态和线上问题对上号的人不多。写这个主题,是因为我见过太多人把线程状态当八股文背:new、runnable、blocked、waiting、timed_waiting、terminated,六种状态倒背如流,但一遇到线上CPU飙升、服务卡顿、线程池打满,就不知道从哪儿下手。这篇博文想做的,是把六种状态从“面试答案”变成“排查工具”。我会从Thread.State的源码设计讲起,把每种状态的进入条件、退出条件、常见误区说清楚,再带你把代码跑起来,用jstack和ThreadMXBean亲眼看看线程在不同场景下到底处于什么状态。最后整理我在实际排查中踩过的坑:死锁长什么样、线程池里一堆TIMED_WAITING算不算故障、Lock和synchronized对状态标签有什么影响。适合准备Java面试的人,也适合正在被线上并发问题折磨的开发者——尤其是那种“明明没报错,服务就是不响应”的场景,线程dump往往一眼就能告诉你答案。

1. 先别背答案:线程状态是用来解决问题的,不是用来考试的

1.1 线程状态是JVM给你的一把诊断钥匙

很多人把线程状态理解成“线程在干什么的标签”,这个理解没错,但太表面了。线程状态的真正价值在于:它是JVM对外暴露的、用来回答“这个线程为什么没有继续执行”的标准答案。

想象一个场景:线上接口突然变慢,请求堆积。你第一反应是什么?看日志、看监控、看数据库慢查询。但如果这些都正常,下一步就是抓线程dump,看线程到底卡在哪里。这时候,六种状态就是你排查问题的地图:如果线程卡在BLOCKED,说明它在等锁;如果卡在WAITING,说明它在等某个信号;如果大量线程都在RUNNABLE且CPU飙升,说明它们在争抢CPU或陷入死循环。没有这个地图,你面对的就是一堆栈信息,看不出规律。

换个角度理解:JVM线程状态本质上是对“线程调度与同步机制”的一种抽象。线程调度是操作系统的事,线程同步是JVM和类库的事,但最终都要反映到状态上。搞懂了状态,你就同时抓住了调度和同步这两条线,排查并发问题就有了方向感。

1.2 为什么是六种而不是五种或八种

先看Thread.State的源码定义。Java的Thread类里有一个State枚举,总共六个值:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。

public enum State { NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED; }

对比操作系统线程的五种状态(新建、就绪、运行、阻塞、终止),你会发现Java把“就绪”和“运行”合并成了RUNNABLE。这是刻意的设计取舍:Java线程的调度完全委托给操作系统,JVM层面无法也不想知道线程此刻是被分配到了CPU正在执行,还是在等待CPU调度。在JVM看来,这两种情况都“能跑没跑”,统一标记为RUNNABLE最省事,也避免在用户态做无意义的二次判断。

整个状态设计其实可以拆成两组:第一组是生命周期三态——NEW(刚创建)、RUNNABLE(能执行)、TERMINATED(已结束);第二组是“等三态”——BLOCKED(等锁)、WAITING(无限期等信号)、TIMED_WAITING(限期等信号或等时间)。搞清这两组划分,你再看jstack输出就不会发懵。网上有些文章说Java有“新建、就绪、运行、阻塞、等待、超时等待、终止”七种状态,那是把操作系统的“就绪/运行”拆出来和JVM的RUNNABLE混在一起说了,严格来讲Thread.getState()只可能返回那六个枚举值,不会有第七种。

2. 六种状态逐个过:什么时候进、什么时候出、坑在哪

2.1 NEW——new出来的线程还不是“线”

NEW是线程创建后、调用start()之前的状态。这一步最容易被忽略的细节是:new Thread()只是创建了一个Thread对象,JVM还没有为它分配任何操作系统线程资源。也就是说,一个NEW状态的线程,在操作系统层面根本不存在对应的线程实体,它就是一个普通的Java对象。

这个细节有个直接后果:只有调用start()后,线程才会真正被创建并开始执行run()方法。而且start()不能重复调用,第二次调用会抛出IllegalThreadStateException。我见过不少新手在这上面栽跟头,想“重启”一个线程,发现start()第二次直接异常,就是因为线程到了TERMINATED之后无法回到NEW,生命周期是不可逆的。

实操上有两个习惯我建议养成:一是创建线程务必指定一个有意义的线程名,用ThreadFactory或者在new Thread里传name参数,不然后面jstack看到“Thread-12”根本猜不到它是干什么的;二是能用线程池就别new Thread(),线程是稀缺资源,创建和销毁的开销都不小,池化能显著降低这个成本。

2.2 RUNNABLE——既是就绪,也是运行

只要线程调用start()之后没终止,并且不在“等三态”里,它就在RUNNABLE。前面说过,RUNNABLE合并了操作系统的“就绪”和“运行”两种状态,所以一个RUNNABLE线程可能正占着CPU执行,也可能排在等待队列里等着被调度。它在JVM里统一就是RUNNABLE,不会细化。

这个状态有一个特别迷惑人的点:jstack里看到RUNNABLE并不意味着CPU一定在执行它,更不能直接断定它在干活。比如线上一个线程在做socket读取,网络包一直不来,它可能在用户态等待IO数据,状态依然是RUNNABLE。再比如自旋锁的线程,CPU空转等待锁释放,状态也是RUNNABLE。所以看到RUNNABLE先别急着下结论“它在忙”,要结合CPU占用和栈信息综合判断。

RUNNABLE相关的还有一个经典面试题:Thread.yield()会让线程变成什么状态?答案是它仍然是RUNNABLE。yield()只是主动让出CPU,让其他同优先级线程有机会执行,但线程本身依然是“可运行”的,调度器随时可以再把它调度回来,并没有进入任何阻塞或等待状态。

2.3 BLOCKED——被锁挡在门口等

BLOCKED是并发里最容易出问题的状态,也是面试最高频的坑。它特指线程因竞争synchronized监视器锁而阻塞,在等待进入synchronized方法或代码块时,线程会从RUNNABLE变成BLOCKED。注意这里的关键词是“synchronized”,用Lock接口(比如ReentrantLock)等待锁的线程状态不是BLOCKED,而是WAITING,这个我在后面第五节展开讲。

怎么理解BLOCKED?想象一群人抢一个卫生间,门锁着的时候,后面的人都得在门口排队。这些排队的人就是BLOCKED状态。一旦持锁线程释放锁,JVM就会从等待队列里挑一个线程让它获得锁并开始执行,注意是“挑一个”而不是“按先来后到的顺序都放进去”。

实操中判断BLOCKED有没有异常,要看数量级和持续时间。如果一两个线程短时间BLOCKED,这是正常的锁竞争;如果大量线程长时间BLOCKED,说明锁的粒度太粗、持有时间太长,或者出现了锁嵌套导致的死锁风险。jstack中BLOCKED线程通常伴随“(on object monitor)”标记,下面那行栈信息就指向它正在等的锁。

2.4 WAITING——没有期限的等待,最容易被忽略

WAITING状态表示线程在无限期等待另一个线程执行特定操作,没有超时时间。三个最常见的进入方式:Object.wait()(等notify/notifyAll)、Thread.join()(等目标线程结束)、LockSupport.park()(等unpark)。

先说Object.wait(),它必须是synchronized块内才能调用,否则会抛IllegalMonitorStateException。wait()一旦被调用,线程会释放持有的监视器锁,然后进入WAITING。网上很多人把wait和sleep搞混,这里有个本质区别:sleep不释放锁,wait释放锁。所以wait经常配合notifyAll用于生产者消费者模型,让出锁让别人干活。

Thread.join()也容易踩坑。join()的语义是“当前线程等待调用join()的那个线程终止”,例如在main线程里调用t.join(),main线程就进入WAITING等t跑完。它的内部实现其实是基于wait/notify机制——join()的本质是调用wait(0)无限期等待,目标线程结束时会触发notifyAll。很多人以为join只是“等一下”,不知道它会影响当前线程状态,这在排查“主线程卡住不动”时是个盲点。

LockSupport.park()是AQS(AbstractQueuedSynchronizer)的基础工具,ReentrantLock、CountDownLatch、Semaphore这些并发工具内部的等待,底层都是park。调用park()后线程进入WAITING状态,jstack显示为“WAITING (parking)”。park和wait有个关键区别:park不会释放锁,它纯粹是“挂起当前线程”,跟synchronized监视器无关。所以用Lock等锁时,线程持有着自己那套锁队列的关系,而不会被synchronized的monitor机制感知。

2.5 TIMED_WAITING——带闹钟的等待

TIMED_WAITING和WAITING的唯一区别就是有没有超时时间。等待到期后线程自动唤醒,不需要别人通知。常见的进入方式:Thread.sleep(ms)、wait(timeout)、join(timeout)、LockSupport.parkNanos(ns)、LockSupport.parkUntil(deadline)。

sleep是最常见的TIMED_WAITING来源。注意两点:sleep是“抱着锁睡觉”,它不会释放任何监视器锁。如果代码里在synchronized块中调用sleep,其他线程依然被挡在外面,这也是“为什么synchronized块里不能随便sleep”的原因。另外sleep可以被interrupt打断,打断后会抛出InterruptedException,这是处理优雅停机时绕不开的点。

wait(timeout)和sleep在状态上都是TIMED_WAITING,但行为不同:wait(timeout)会释放锁,超时后重新竞争锁才继续执行。这个区别体现在状态转场上:wait(1000)在等待期间,另一个线程如果也synchronized同一把锁,是能进去的;而sleep(1000)在等待期间,其他线程只能干等。

jstack里TIMED_WAITING的标记会带后缀,比如“TIMED_WAITING (sleeping)”“TIMED_WAITING (on object monitor)”或“TIMED_WAITING (parking)”,分别对应sleep、wait(timeout)和parkNanos/parkUntil这几种来源,看日志时可以通过后缀快速判断线程卡在哪种API上。

2.6 TERMINATED——线程的终点站

TERMINATED是线程执行完run()方法后进入的状态,包括run()正常返回、run()抛出未捕获异常导致线程退出两种情况。还有一点:就是线程可以调用stop()强行终止,但这个方法早已被标记为过时危险,它会释放线程持有的所有监视器锁,却不会保证状态的一致性,极端情况下会把对象数据弄到一半就扔掉,生产环境严禁使用。

处于TERMINATED的线程不能再调用start(),否则抛IllegalThreadStateException。线程一旦结束,其生命周期就走完了,这是前面“生命周期不可逆”的直接体现。

在实际排查中,我们一般不关心TERMINATED的数量,它只表示历史任务的结束。需要关注的往往是存活线程的状态分布:哪些在RUNNABLE跑业务,哪些BLOCKED在等锁,哪些WAITING在休眠。状态分布能直接反映服务健康度,比如线程池满负载时,大量线程卡在BLOCKED或WAITING,新的任务进不了执行队列,接口就慢慢变得不可用。

3. 实操:写代码让线程“变身”,再用JVM自带工具实地看一把

3.1 写一个小Demo,亲眼看到六种状态切换

理论说得再多,不如跑一遍代码直观。我平时教学时会写一个状态观察器:创建若干线程,让它们分别进入不同的状态,然后用主线程定时打印所有线程的当前状态。

先看最简单的生命周期演示:

public class ThreadStateDemo { public static void main(String[] args) throws Exception { Thread t = new Thread(() -> { try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); System.out.println("未start: " + t.getState()); // NEW t.start(); Thread.sleep(50); System.out.println("运行中: " + t.getState()); // TIMED_WAITING,因为线程在sleep t.join(); System.out.println("结束后: " + t.getState()); // TERMINATED } }

这段代码的输出会依次是NEW、TIMED_WAITING、TERMINATED。注意中间那个结果是TIMED_WAITING而不是RUNNABLE,是因为线程启动后很快进入了sleep(2000),主线程在它sleep期间抓到的状态自然是TIMED_WAITING。如果想看到RUNNABLE状态,可以让线程跑一段纯计算任务不sleep,主线程再去抓。

接着看BLOCKED和WAITING。需要两个线程配合:线程A持有锁并sleep,线程B去争这把锁,就能看到BLOCKED;在线程B的synchronized块里调用wait(),它就会变成WAITING:

public class BlockedWaitingDemo { static final Object lock = new Object(); public static void main(String[] args) throws Exception { Thread holder = new Thread(() -> { synchronized (lock) { System.out.println("holder 拿到锁,开始sleep"); try { Thread.sleep(3000); } catch (InterruptedException e) {} System.out.println("holder 释放锁"); } }, "holder-thread"); holder.start(); Thread.sleep(100); Thread blockedThread = new Thread(() -> { synchronized (lock) { System.out.println("blockedThread 进入锁内"); } }, "blocked-thread"); blockedThread.start(); Thread.sleep(100); System.out.println("blocked-thread 状态: " + blockedThread.getState()); // BLOCKED Thread waitingThread = new Thread(() -> { synchronized (lock) { try { lock.wait(); } catch (InterruptedException e) {} } }, "waiting-thread"); waitingThread.start(); Thread.sleep(100); System.out.println("waiting-thread 状态: " + waitingThread.getState()); // WAITING holder.join(); System.out.println("holder-thread 状态: " + holder.getState()); // TERMINATED } }

这里有个细节值得琢磨:holder线程sleep了3000毫秒,期间一直持有lock,所以blockedThread进不去synchronized块,体现为BLOCKED。waitingThread虽然也在等锁,但它是在尝试进入synchronized块时被挡住的,所以也是BLOCKED——注意,只有真正进入了synchronized块内部,调用lock.wait()之后才会变成WAITING。这两个状态在jstack里的栈信息位置完全不同,一个指向synchronized入口,一个指向wait()调用点。

3.2 jstack、jcmd和VisualVM:不用IDE也能看状态

代码里调getState()方便调试,线上排查还得靠JVM自带工具。最常用的是jstack,用法很简单:

jstack <pid> jstack <pid> > thread_dump.log

获取到dump文件后,重点看线程名和java.lang.Thread.State这两行。jstack会自动标注每个线程的状态,比如:

"blocked-thread" #20 prio=5 os_prio=0 cpu=0.33ms elapsed=12.34s tid=0x00007f9d0c1a1800 nid=0x2eea waiting for monitor entry [0x00007f9d04b76000] java.lang.Thread.State: BLOCKED (on object monitor) at BlockedWaitingDemo.lambda$main$2(BlockedWaitingDemo.java:25) - waiting to lock <0x00000007acd8d0d8> (a java.lang.Object)

看到“waiting to lock”,说明它正在等锁,下面还有一把锁的地址,和持锁线程的“locked <同一个地址>”对应起来,就能还原锁的竞争关系。

除了jstack,jcmd也一样好用,命令是jcmd <pid> Thread.print,输出格式和jstack基本一致。VisualVM则适合本地开发时看状态演变,连接本地进程后能在“线程”面板看到每个线程的状态实时变化,对学习阶段理解状态迁移特别直观。

我在排查线上问题时习惯这样操作:先top -Hp pid看哪个线程占用CPU高,拿到线程的十进制PID转成十六进制,再到jstack输出里用nid匹配,定位到具体栈。这套流程能快速找到“最忙”的线程在干什么,比漫无目的地翻状态列表高效得多。

3.3 手动推导一遍状态流转,别让概念只停留在纸上

看完代码和dump,最后把状态流转在脑子里过一遍。从这个流转图中你能看到几个关键结论:生命周期三态是单向直行的,NEW只能进RUNNABLE,RUNNABLE只能进TERMINATED;而“等三态”都是可逆的,RUNNABLE可以任意切到BLOCKED、WAITING、TIMED_WAITING,也都能切回RUNNABLE。状态之间不会跨级跳转,比如WAITING不会直接变BLOCKED,必须先回到RUNNABLE,再由JVM调度到锁等待队列里。

NEW -> RUNNABLE: start()调用成功 RUNNABLE -> BLOCKED: 进入synchronized但锁被占用 RUNNABLE -> WAITING: wait()/join()/park() RUNNABLE -> TIMED_WAITING: sleep()/wait(timeout)/join(timeout)/parkNanos() BLOCKED -> RUNNABLE: 成功获取监视器锁 WAITING -> RUNNABLE: notify()/notifyAll()/unpark()/目标线程终止 TIMED_WAITING -> RUNNABLE: 等待超时/被唤醒/interrupt() RUNNABLE -> TERMINATED: run()执行完毕或抛出异常

4. 线上排查实录:死锁、线程泄漏和那些伪装成BUG的状态

4.1 死锁到底长什么样:状态、栈、锁地址三要素

死锁是线程状态最典型的应用场景。两个线程各自持有一把锁,又都在等对方手里的锁,于是互相等待,谁也跑不了。代码写出来很简单:

public class DeadLockDemo { static final Object lockA = new Object(); static final Object lockB = new Object(); public static void main(String[] args) { Thread t1 = new Thread(() -> { synchronized (lockA) { try { Thread.sleep(50); } catch (InterruptedException e) {} synchronized (lockB) { System.out.println("t1 get lockB"); } } }); Thread t2 = new Thread(() -> { synchronized (lockB) { try { Thread.sleep(50); } catch (InterruptedException e) {} synchronized (lockA) { System.out.println("t2 get lockA"); } } }); t1.start(); t2.start(); } }

跑起来后jstack的dump里,最上面会有一段“Found one Java-level deadlock”的检测报告,直接把两个线程的栈、锁地址和持有的锁全部列出来。即使没有这段报告,光看状态也能看出来——两个线程都卡在BLOCKED,而且栈里都有“waiting to lock <地址>”和“locked <地址>”交替出现,另一个线程正好锁住了它想要的地址。

死锁只是“线程互相等锁”的一种极端情况。更隐蔽的是资源嵌套等待:三个线程各持有一把锁,形成环形等待。JVM的jstack只能检测有向锁依赖图里的环,对某些复杂的依赖不一定能完全识别出来,这时候就得靠ThreadMXBean提供的findDeadlockedThreads()做编程式检测,把结果接到监控告警里。

4.2 线程池里一堆TIMED_WAITING,先别急着报警

线程池的引入让线程状态又多了一层业务语义。默认情况下,ThreadPoolExecutor里的核心线程只要没任务可做,就会在阻塞队列上等待新任务,状态是WAITING或TIMED_WAITING,具体看用了哪种队列和策略。这是正常现象,不是故障。如果你在jstack里看到几个核心线程都在TIMED_WAITING或WAITING,说明当前任务量不大,它们在“待命”。

但如果线程池的队列堆积了成千上万个任务,而工作线程全都在WAITING(比如任务里都在等数据库连接池的连接),那这就是典型的“连接池耗尽”问题:业务线程被慢查询或外部接口拖住,线程池的所有线程都在等IO资源,新的请求全部排队,接口响应时间直线上升。这时候jstack里能看到大量WAITING的线程栈指向同一个LockSupport.park调用,下面叠着数据库驱动或HTTP客户端的等待逻辑。

线程池还有一个常见的隐藏坑:核心线程的keepAliveTime。默认情况下核心线程即使空闲也不会被回收,只有非核心线程超时会被回收。如果设置了allowCoreThreadTimeOut(true),核心线程也会在空闲超过keepAliveTime后退出,这时候线程数量会掉下来,下次再来任务再创建,线程状态会频繁在RUNNABLE和TERMINATED之间切换,吞吐量反而受影响。要不要开这个开关,取决于你的业务是追求固定的低延迟还是更在意资源占用,没有绝对标准。

4.3 假死锁:线程看起来都在等,其实是在sleep

排查经验多了之后,最难处理的是“假死锁”。现象是服务无响应,jstack打开一看,大量线程处于TIMED_WAITING。很多人的第一反应是线程都在等待锁,要往死锁方向查。但仔细看栈信息,会发现它们是“TIMED_WAITING (sleeping)”,线程在sleep,不是在等锁。

这种case一般出在两类代码上:一是业务代码里直接调了Thread.sleep,用来做重试退避或限速,sleep期间线程不干活,但这个线程还占着线程池的worker名额,任务多的时候就像“占着茅坑不拉屎”一样把线程池拖满;二是框架内部有定时轮询逻辑,比如某个客户端每隔几百毫秒主动拉取一次状态,轮询间隔里线程就sleep。

排查假死锁的关键,是看栈顶的调用栈是sleep还是park、是wait(timeout)还是synchronized等锁。sleep是不释放锁也不释放线程池名额的等待,park在AQS的等待队列里,语义不同。为了尽量避免在业务代码里裸用sleep,建议优先使用带超时参数的Lock或信号量做等待,或者把sleep的时间调小并配合循环,这样既能给其他任务让路,也能在dump里留下更清晰的调用意图。

5. 再往深走一层:锁、原子类和虚拟线程对线程状态的影响

5.1 synchronized和ReentrantLock,影响的其实是状态标签

“线程状态”这个概念不能和“锁的实现”割裂开看,因为你用不同的锁,线程进入的状态标签完全不一样。synchronized是JVM在monitor层面上做的互斥,竞争不到monitor entry时,线程进入BLOCKED,jstack会标“(on object monitor)”。ReentrantLock是JUC在Java层面用AQS实现的锁,等锁的线程调用的是LockSupport.park(),因此状态是WAITING,jstack标“(parking)”。

这个区别不是抠字眼,而是有实际影响的。BLOCKED状态默认不可中断——线程在等synchronized锁时,调用interrupt()只是设置中断标志,线程不会因为中断而停止等待;而ReentrantLock的lockInterruptibly()在等待时收到中断信号会立刻抛出InterruptedException,让线程有机会从等待中脱身。所以如果业务上需要“等待锁必须可以被取消”,synchronized做不到,只能用Lock。

再比如锁超时。synchronized没有“tryLock(timeout)”这种超时获取的机制,拿不到锁就一直等。ReentrantLock.tryLock(3, TimeUnit.SECONDS)等不到就返回false,线程可以走其他分支,不至于无限期卡在WAITING里。所以从线程状态可观测、可干预的角度看,Lock比synchronized给排查和处理留了更多余地。

5.2 原子类和CAS:无锁并发时,线程真的不会进BLOCKED

既然等锁会阻塞,那有没有办法不让线程阻塞?有,就是无锁编程,核心是CAS(Compare-And-Swap)。以AtomicInteger为例,它内部维护了一个volatile的value,每次自增都用CAS去试着把当前值替换成新值,成功了就返回,失败就继续循环尝试。整个过程一直在RUNNABLE状态里自旋,不会进入BLOCKED或WAITING。

CAS自旋意味着线程不释放CPU,它在“忙等”。如果竞争不激烈,自旋几次就成功了,性能比锁高得多;如果竞争激烈到几百上千个线程同时抢一个AtomicInteger,CPU会被白白烧掉大量时间片。所以“无锁“并不是没有代价,它把“等待”从线程状态层面转移到了CPU时间片层面——线上看到线程都在RUNNABLE但CPU飙得很高,有时就是CAS自旋在空转。

这个思路延伸到很多高性能框架里:比如一些内存队列用CAS+volatile数组实现,多生产者多消费者场景下消费者线程没有数据时会park,有数据时unpark唤醒,而不是让线程死锁或忙等。状态设计要兼顾“及时响应”和“资源开销”,park唤醒的代价比自旋低,但比锁竞争高,工程实现里经常要在这之间做取舍。

5.3 虚拟线程让“阻塞”不再是资源黑洞

聊线程状态躲不开一个新话题:Java 21正式发布的虚拟线程(Virtual Threads)。虚拟线程最大的变化不在状态枚举本身——它依然有NEW、RUNNABLE、WAITING这些状态——而在底层实现:虚拟线程由JVM调度,挂起和恢复不再依赖操作系统线程,创建成本极低。也就是说,以前线程池要刻意控制线程数量,因为线程是稀缺资源;虚拟线程可以把“每个请求一个线程”变成“每个任务一个虚拟线程”,阻塞等待不再是浪费资源的事。

这对线程状态排查的影响是深远的。以前看到一个线程WAITING,你得判断它是不是占着线程池名额导致资源耗尽;现在如果跑在虚拟线程上,一个WAITING的虚拟线程不占用OS线程,它在等IO时背后的载体线程可以去跑别的虚拟线程。阻塞的代价被大幅降低,线程状态作为资源评估指标的权重也下降了。

不过要提醒一句:虚拟线程不是万能药。CPU密集型任务在虚拟线程上跑,该占CPU还是占。用jstack看虚拟线程的dump也和普通线程不一样,需要用jcmd 的Thread.dump_to_file配合--format=json来查看虚拟线程的挂起栈,这个等以后专门写一篇再展开。先把经典线程状态这课补扎实,再看虚拟线程就不会被它表面的“像线程又不是线程”绕晕。

我在实际排查中有一个习惯:遇到服务卡顿或者偶发超时,不会一上来就翻日志,而是先抓三份jstack,间隔几秒钟各抓一次,然后再分析。三次dump能看出线程状态有没有在变化,如果某几个线程三次都卡在同一个栈的同一行,基本可以断定它们在“原地等待”;如果栈在变,说明线程还在往前走,只是慢。这套方法帮我解决过不少“看起来像死锁其实不是”的诡异问题。再分享一个小技巧:在应用的启动脚本里预置一段jstack自动执行的逻辑,服务出问题时先留痕迹再重启,别急着把现场关掉,很多时候重启一切归零,问题也就再也查不到了。最后建议每个人花半小时亲手跑一遍上面的Demo,看一看getState()在不同时刻的输出,再用jstack对照一下,这一轮走下来,比你背十遍状态定义都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询