【无标题】

Java 线程的 6 种状态,我画了三张图才搞明白

我刚开始学多线程的时候,最懵的不是 synchronized 怎么写,也不是线程池参数怎么配,而是线程那一堆状态。BLOCKED 和 WAITING 到底有什么区别?为什么线程明明在等东西,状态却显示 RUNNABLE?有一回我写了个死锁 bug,dump 出线程堆栈一看,满屏的 BLOCKED 和 WAITING,完全看不懂谁在等谁。后来花了两个下午把线程状态从头捋了一遍,画了三张草图,终于通了。


一张图先看全局

在讲每个状态之前,先把 6 种状态和它们之间的流转画出来。不用画多好看,知道谁连着谁就行:

这张图不用背,看多了自然记住。先往下看每个状态是干什么的,回头再看这张图会清晰很多。


1. NEW:刚出生,还没开始干活

java 复制代码
Thread t = new Thread(() -> {
    System.out.println("我在干活");
});
// 此时 t 处于 NEW 状态
System.out.println(t.getState());  // 输出: NEW

new Thread() 之后、start() 之前,这个线程只是一个普通的 Java 对象,躺在堆内存里。操作系统的线程资源还没分配,它什么也干不了,跟 new String() 没什么本质区别。

你可以把 NEW 理解成:招了一个员工,签了合同,但还没让他进办公室。工位没分配,电脑没领,他只是名册上的一个名字。


2. RUNNABLE:就绪和运行,Java 不区分

调了 start() 之后,线程就进入 RUNNABLE 状态。但这里有一个新手很容易踩的概念坑:

Java 的 RUNNABLE 包含了操作系统层面的两个状态------就绪运行中。在操作系统眼里,"等着 CPU 分配时间片"和"正在 CPU 上执行"是两回事。但在 Java 眼里,它们都叫 RUNNABLE。

java 复制代码
Thread t = new Thread(() -> {
    while (true) {
        // 死循环,一直在 CPU 上跑
    }
});
t.start();
System.out.println(t.getState());  // 输出: RUNNABLE

Thread t2 = new Thread(() -> {
    // 线程已就绪,等待 CPU 调度,也是 RUNNABLE
});
t2.start();
System.out.println(t2.getState()); // 输出: RUNNABLE

为什么 Java 不区分这两个?因为区分了对开发者意义不大。你写的代码不关心线程这一纳秒是在 CPU 上跑还是在排队,你只需要知道"它能跑"。分得太细反而增加心智负担。

打个比方:一个餐厅后厨有 4 个灶台,10 个厨师。正在炒菜的厨师是"运行中",站在旁边等灶台的是"就绪"。但在大堂经理眼里,这 10 个人都在"工作状态",他不需要区分谁在颠勺谁在等锅。


3. BLOCKED:卡在门口,等别人用完锁

java 复制代码
final Object lock = new Object();

Thread t1 = new Thread(() -> {
    synchronized (lock) {
        // t1 拿到了锁,在里面慢慢干活
        Thread.sleep(5000);
    }
});
t1.start();
Thread.sleep(100);  // 保证 t1 先进同步块

Thread t2 = new Thread(() -> {
    synchronized (lock) {
        // t2 想进来,但锁被 t1 拿着,只能等
    }
});
t2.start();
Thread.sleep(100);

System.out.println(t2.getState());  // 输出: BLOCKED

BLOCKED 只跟 synchronized 有关。多个线程抢同一把锁,抢到的线程进去干活,没抢到的就站在门口等。门一开,这堆等着的人里只有一个能进去。

注意 BLOCKED 的特点:

  • 它等的是一个具体的锁对象
  • JVM 会帮你管理这个等待队列,不需要你手动处理
  • 一旦持有锁的线程出了同步块(正常退出或者抛异常),JVM 会从等待队列里挑一个线程放进去

最形象的理解就是公共厕所。里面有人,你在外面站着等,这就是 BLOCKED。里面人出来,你进去。


4. WAITING:等着别人来叫你,自己不会醒

java 复制代码
final Object lock = new Object();

Thread waiting = new Thread(() -> {
    synchronized (lock) {
        lock.wait();  // 释放锁,进入 WAITING
    }
});
waiting.start();
Thread.sleep(100);

System.out.println(waiting.getState());  // 输出: WAITING

// 另一个线程来唤醒它
new Thread(() -> {
    synchronized (lock) {
        lock.notify();
    }
}).start();

WAITING 和 BLOCKED 最大的区别在哪?

BLOCKED 是被动等锁 ,锁释放了就自动去抢,你不用管。WAITING 是主动等人叫,没人叫你你就一直等下去,等到天荒地老。

触发 WAITING 的常见方法:

  • Object.wait():在同步块里调,会释放当前持有的锁,然后进入等待
  • Thread.join():等另一个线程执行完
  • LockSupport.park():底层的挂起方法

WAITING 状态的线程不会被 CPU 调度,不会浪费 CPU 资源。它就像你在工位上睡着了,得有人来拍你肩膀你才会醒。

我刚开始学的时候,搞混 BLOCKED 和 WAITING 搞了很久。其实很简单:BLOCKED 的线程是在"抢东西",WAITING 的线程是在"等人叫"。动机不一样。


5. TIMED_WAITING:设了闹钟的等待

java 复制代码
Thread sleeping = new Thread(() -> {
    Thread.sleep(3000);  // 睡 3 秒
});
sleeping.start();
Thread.sleep(100);

System.out.println(sleeping.getState());  // 输出: TIMED_WAITING

TIMED_WAITING 就是带超时的 WAITING。你定了个时间,时间一到自动醒,不需要别人叫你。

常见的触发方法:

  • Thread.sleep(long millis):让当前线程睡一段时间,不释放锁
  • Object.wait(long timeout):等一段时间,超时自动醒
  • Thread.join(long millis):等另一个线程一段时间
  • LockSupport.parkNanos()parkUntil():底层定时挂起

这里有一个面试经常问、新手经常搞错的点:sleep() 不会释放锁。

java 复制代码
synchronized (lock) {
    Thread.sleep(5000);  // 这 5 秒内锁还是你的,别人进不来
}

wait() 会释放锁:

java 复制代码
synchronized (lock) {
    lock.wait(5000);  // 调用 wait 的瞬间锁就释放了,别人能进来
}

这个区别在写生产者消费者模式的时候特别重要。用 sleep 代替 wait 是新手很容易犯的错误,程序看起来在跑,实际上完全不是你想要的行为。


6. TERMINATED:干完了,走人了

java 复制代码
Thread t = new Thread(() -> {
    System.out.println("干完了");
});
t.start();
t.join();  // 等 t 执行完

System.out.println(t.getState());  // 输出: TERMINATED

线程的 run() 方法正常执行完,或者抛了未捕获的异常,线程就进入 TERMINATED 状态。这是终态,不能再启动了。

java 复制代码
Thread t = new Thread(() -> System.out.println("跑一次"));
t.start();
t.join();

t.start();  // 抛异常: IllegalThreadStateException

一个线程对象只能启动一次。想再跑,得 new 一个新的。

这个其实很好理解:你不可能让一个已经退休的员工重新回到工位上,你只能重新招一个人。


状态流转的几个关键点

NEW 只能到 RUNNABLE,而且是单行道。

调了 start() 之后,线程就从 NEW 变成 RUNNABLE。没有回头路。不能从 RUNNABLE 退回 NEW,就像你没法让一个已经在干活的人变回"还没入职"。

TERMINATED 是终点站。

到了 TERMINATED,线程的生命就结束了。不管之前经历了多少次 BLOCKED → RUNNABLE → WAITING → RUNNABLE 的反复横跳,一旦到了 TERMINATED,游戏结束。

中间的四个状态可以来回转。

RUNNABLE、BLOCKED、WAITING、TIMED_WAITING 这四个状态之间可以互相转换。一个线程可能今天被锁挡在外面(BLOCKED),明天抢到锁进去干活了(RUNNABLE),后天调了个 wait() 开始等人(WAITING),大后天被人叫醒又回去干活。


一个容易踩的坑:阻塞 I/O 显示为 RUNNABLE

有一次我在排查一个性能问题,发现某个线程在等数据库返回结果,卡了好几秒。我下意识以为它会显示 WAITING 或者 BLOCKED,结果 dump 出来一看,状态是 RUNNABLE。

当时很困惑。后来查资料才明白:传统的阻塞 I/O(比如老式的 Socket 读写、JDBC 的数据库查询),线程在操作系统层面确实是阻塞的,但 Java 不把它标记为 WAITING 或 BLOCKED,而是继续显示 RUNNABLE。

因为 JVM 不感知底层 I/O 的阻塞。在 JVM 看来,线程就是在执行 socketInputStream.read() 这个 native 方法,它"在跑",所以是 RUNNABLE。至于这个 native 方法内部其实在等网卡数据,JVM 管不着。

这是面试里比较少问到、但排查问题时常遇到的细节。


我当时踩的坑

我刚学多线程的时候,写过一个生产者消费者的小练习。

生产者线程往队列里放东西,消费者从队列里取。我的消费者逻辑大概是这样的:

java 复制代码
while (true) {
    if (queue.isEmpty()) {
        Thread.sleep(100);  // 没东西就睡一会儿再查
    } else {
        String item = queue.poll();
        System.out.println("消费: " + item);
    }
}

看起来能跑。但实际上这是用 sleep 在轮询,每隔 100 毫秒醒来看一眼队列空不空。大多数时候队列都是空的,线程就在反复睡、醒、看、再睡的循环里打转,白白消耗 CPU 上下文切换的开销。

正确的做法是用 waitnotify

java 复制代码
// 消费者
synchronized (queue) {
    while (queue.isEmpty()) {
        queue.wait();  // 没东西就等着,释放锁
    }
    String item = queue.poll();
}

// 生产者
synchronized (queue) {
    queue.add(item);
    queue.notify();  // 放了东西,叫醒消费者
}

消费者在队列空的时候进入 WAITING 状态,完全不占 CPU。生产者放完东西叫一声,消费者才醒过来干活。

这个对比让我真正理解了 TIMED_WAITING 和 WAITING 的区别不只是"有没有超时",而是两种完全不同的编程思路:一个是轮询,一个是通知。前者是每隔几秒去敲一次门,后者是门上贴个条"货到了叫我"。


一张表总结

状态 什么时候进入 什么时候退出 本质
NEW new Thread() start() 只是普通 Java 对象,系统线程资源未分配
RUNNABLE start() 被调用,或者从等待/阻塞状态恢复 抢锁失败、调 wait/sleep/join、执行完毕 要么在跑,要么等着跑
BLOCKED synchronized 锁失败 抢到锁 卡在门口,等锁
WAITING wait()/join()/park() notify()/unpark() 唤醒 主动等人来叫,不设闹钟
TIMED_WAITING sleep(ms)/wait(ms)/join(ms) 超时到期,或者被提前唤醒 设了闹钟的等待
TERMINATED run() 执行完或抛未捕获异常 无法退出,是终态 生命周期结束

怎么拿到线程状态

代码里直接调 getState() 就行:

java 复制代码
Thread t = new Thread(() -> {});
System.out.println(t.getState());  // NEW

t.start();
System.out.println(t.getState());  // 大概率 RUNNABLE(取决于调度时机)

排查线上问题的时候,可以用 jstack <pid> 打出线程堆栈,每个线程的状态一目了然。刚入门的话,建议自己写几个小 demo 故意制造各种状态(死锁搞出 BLOCKED,wait 搞出 WAITING,sleep 搞出 TIMED_WAITING),然后用 jstack 看一眼输出长什么样。亲眼看一次,比看十篇文章印象深。


六个状态其实不多,主要就记三件事:BLOCKED 是在抢锁,WAITING 是在等人叫,TIMED_WAITING 是设了闹钟的等人叫。RUNNABLE 是干活和等活干的统称,NEW 和 TERMINATED 是起点和终点。

写多线程代码出 bug 的时候,第一反应应该是先看线程处在什么状态。状态对了,定位问题就对了一半。