《Java 并发编程·线程与内存模型》1.3 生命周期、优先级与守护线程

六个线程状态不是文档里的名词,而是 jstack 里能对上的现场:用 latch 与监视器构造 NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED 并贴实测输出,解释 BLOCKED 与 WAITING 的排障差异,再说明优先级不可靠与守护线程退出时 finally 不执行的边界。

承接的问题

前两节解决了「线程是什么」和「任务怎么交给线程」。这一节处理剩下的一组状态问题:一个线程从 new 到结束,中间会经过哪些状态;为什么线上转储里有的线程是 BLOCKED,有的是 WAITING;setPriority(10) 能不能让重要任务先跑;把线程设成 daemon 之后,为什么 finally 里的收尾代码有时不执行。

这些问题在写业务代码时很少遇到,但在排障时每天都会遇到。线程转储里最权威的一行就是 java.lang.Thread.State,读不懂它,后面的栈帧分析就没有方向。

六个状态与迁移

Thread.State 是一个枚举,只有六个值:

状态含义进入方式离开方式
NEW已创建,未启动new Thread(...) 或 unstarted()start()
RUNNABLE可运行start() 之后抢锁、等待、睡眠、结束
BLOCKED等待监视器锁进入 synchronized 时锁被占用拿到锁
WAITING无限期等待wait()、join()、park()notify/notifyAll、目标线程结束、unpark、中断
TIMED_WAITING限时等待sleep(n)、wait(n)、join(n)、parkNanos超时、通知、中断
TERMINATED已结束run() 正常返回或抛出未捕获异常不可再迁移

迁移关系可以用一张表固定下来:

当前状态触发动作下一个状态
NEWstart()RUNNABLE
RUNNABLE进入被占用的 synchronizedBLOCKED
BLOCKED锁释放并被调度到RUNNABLE
RUNNABLEwait() / join() / LockSupport.park()WAITING
RUNNABLEsleep(n) / wait(n) / join(n) / parkNanos(n)TIMED_WAITING
WAITING被通知、被中断、目标线程结束RUNNABLE 或 BLOCKED
TIMED_WAITING超时、被通知、被中断RUNNABLE 或 BLOCKED
RUNNABLErun() 返回或抛出异常TERMINATED

两个容易记错的点:

第一,RUNNABLE 不等于「正在使用 CPU」。Java 把「已经启动、等待 OS 调度」和「正在执行」都算作 RUNNABLE。线程阻塞在 socket 读、文件 IO 上时,JVM 通常也只能标成 RUNNABLE,因为底层等待发生在 native 层,JVM 不知道。所以看到大量 RUNNABLE 不要立刻判断是 CPU 打满,要结合 cpu= 时间和系统工具看。

第二,ReentrantLock 等不到锁时不是 BLOCKED。BLOCKED 专指等待 synchronized 的监视器。ReentrantLock.lock() 内部走 LockSupport.park(),状态是 WAITING。这个区别在排查「到底是锁竞争还是条件等待」时非常关键,第 9 章会展开。

用 latch 和监视器把六态跑出来

光看表记不住。下面这个类在同一时刻构造出五种状态,join 之后验证第六种。它可以直接编译运行:

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;

public final class LifecycleProbe {

    private static final Object MONITOR = new Object();

    public static void main(String[] args) throws Exception {
        CountDownLatch entered = new CountDownLatch(1);
        CountDownLatch release = new CountDownLatch(1);

        Thread waiter = Thread.ofPlatform()
                .name("conc-wait")
                .unstarted(() -> {
                    entered.countDown();
                    try {
                        release.await();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                });
        System.out.println("waiter.beforeStart=" + waiter.getState());
        waiter.start();
        entered.await();

        Thread sleeper = Thread.ofPlatform()
                .name("conc-sleep")
                .start(() -> {
                    try {
                        Thread.sleep(5_000);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                });

        AtomicBoolean spinning = new AtomicBoolean(true);
        Thread spinner = Thread.ofPlatform()
                .name("conc-spin")
                .start(() -> {
                    while (spinning.get()) {
                        // busy wait,故意占着 CPU
                    }
                });

        CountDownLatch holderIn = new CountDownLatch(1);
        Thread holder = Thread.ofPlatform()
                .name("conc-holder")
                .start(() -> {
                    synchronized (MONITOR) {
                        holderIn.countDown();
                        try {
                            Thread.sleep(3_000);
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                        }
                    }
                });
        holderIn.await();

        CountDownLatch blockedIn = new CountDownLatch(1);
        Thread blocked = Thread.ofPlatform()
                .name("conc-blocked")
                .start(() -> {
                    blockedIn.countDown();
                    synchronized (MONITOR) {
                        System.out.println("blocked.acquired=true");
                    }
                });
        blockedIn.await();
        Thread.sleep(300);

        System.out.println("waiter=" + waiter.getState());
        System.out.println("sleeper=" + sleeper.getState());
        System.out.println("spinner=" + spinner.getState());
        System.out.println("holder=" + holder.getState());
        System.out.println("blocked=" + blocked.getState());

        release.countDown();
        spinning.set(false);
        waiter.join();
        sleeper.join();
        spinner.join();
        holder.join();
        blocked.join();
        System.out.println("waiter.afterJoin=" + waiter.getState());
        System.out.println("blocked.afterJoin=" + blocked.getState());
    }
}

本机输出:

waiter.beforeStart=NEW
waiter=WAITING
sleeper=TIMED_WAITING
spinner=RUNNABLE
holder=TIMED_WAITING
blocked=BLOCKED
blocked.acquired=true
waiter.afterJoin=TERMINATED
blocked.afterJoin=TERMINATED

对着输出解释每一行:

  • waiter.beforeStart=NEW:unstarted() 创建了线程对象,但 start() 还没调用。此时它不占 OS 线程,只是一个 Java 对象。
  • waiter=WAITING:线程卡在 CountDownLatch.await() 上。await() 内部用 AQS 的 park(),属于无限期等待。
  • sleeper=TIMED_WAITING:Thread.sleep(5_000) 是限时等待。
  • spinner=RUNNABLE:忙循环,始终可运行。
  • holder=TIMED_WAITING:它持有 MONITOR,但正在 sleep。注意这是最容易误判的一行——持有锁的线程在睡觉,它的状态是 TIMED_WAITING,不是 BLOCKED。
  • blocked=BLOCKED:它在 synchronized 入口等 MONITOR,所以是 BLOCKED。
  • blocked.acquired=true 出现在状态打印之后:release 和 holder 的 3 秒 sleep 结束后,blocked 才拿到锁。
  • afterJoin=TERMINATED:join() 返回时线程已经结束,状态不会再变化。

这段实验值钱的地方在于它同时展示了「持锁并睡眠」和「等锁」两种现场。只看名字很容易以为「等锁」和「持有锁」都是 BLOCKED,实际只有等锁才是。

BLOCKED 与 WAITING 的区别

这是线程转储里最常被混淆的一对状态。区别可以列成表:

维度BLOCKEDWAITING
等待对象synchronized 监视器wait()、join()、park() 等条件
是否持有锁不持有目标监视器可能持有别的锁(wait() 会释放目标监视器)
能否被中断不能,中断只设置标志wait/join 会抛 InterruptedException;park 会返回
转储提示waiting to lock <0x...>waiting on condition 或 in Object.wait()
常见原因锁被长时间持有、锁粒度过大条件未满足、通知丢失、下游慢
处理方向缩小临界区、换锁、查死锁查通知逻辑、加超时、查依赖

排障时的判据:

  • 多个线程停在同一个 BLOCKED,且栈顶都是同一行 synchronized,说明锁竞争严重。继续看持有锁的线程在干什么:如果它在做 IO 或睡眠,就是典型的「锁里做慢操作」。
  • 多个线程停在同一个对象的 WAITING,先区分是空闲还是卡死。线程池的工作线程停在 getTask() 的 park 上是健康的;业务线程停在 wait() 上且没人 notify,才是问题。
  • BLOCKED 不会响应中断。想取消一个正在等锁的线程,interrupt() 只能设置标志,它仍要拿到锁才能继续,然后在下一个检查点看到标志。这也是 ReentrantLock.lockInterruptibly() 存在的理由,第 9 章会对比。

还有一条实践原则:不要用 getState() 做线程间协调。状态是瞬时快照,你读到 RUNNABLE 的下一纳秒它就可能变成 TERMINATED。协调要用 CountDownLatch、Future、join 这些有明确 happens-before 关系的工具。

优先级:一个不能依赖的提示

Thread.setPriority(int) 接受 1–10,默认 5。很多旧代码用它来「让重要任务先跑」,这是错的。

Java 优先级只是给操作系统调度器的一个提示,映射关系由平台决定:

平台映射行为
Linux映射到 nice 值,普通用户只能调低不能调高,效果有限
macOS调度器对 Java 优先级的响应很弱,基本可视为忽略
Windows有映射,但与其他进程的优先级共同竞争

更根本的问题是:优先级不解决排队。高优先级线程不会「插队」到已经持有锁的线程前面,也不能让一个阻塞在 IO 上的任务提前返回。用它来保证业务顺序,得到的只是「大多数时候看起来有效」的假象。

优先级唯一合理的用途是给后台维护线程设一个较低的值,减少它与业务线程争抢 CPU 的意愿。即便如此,也不要把正确性建立在它上面。需要顺序,用队列;需要限流,用信号量;需要及时性,用更小的任务粒度和足够的核数。

setPriority 还有一个约束:它可以在线程启动后调用,但效果依赖平台;setDaemon 则必须在 start() 之前调用,否则抛 IllegalThreadStateException。

守护线程:不阻止 JVM 退出

JVM 退出的条件是:所有非守护线程都结束,或者有人调用了 System.exit()。守护线程不参与这个判断。

Thread sweeper = Thread.ofPlatform()
        .name("loan-expiry-sweeper")
        .daemon(true)
        .start(() -> {
            try {
                while (true) {
                    Thread.sleep(1_000);
                    // 清理过期预约
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                System.out.println("sweeper finally");   // 不保证执行
            }
        });

当最后一个非守护线程结束时,JVM 直接开始关闭流程,守护线程被「突然」终止。这里的「突然」意味着:

  • finally 块不保证执行。
  • 方法栈不会正常展开,catch 也不会捕获到任何「退出异常」。
  • 正在写的文件、正在提交的事务可能停在半路。

所以守护线程不能承担必须完成的收尾工作。要保证退出时刷缓冲、关连接、提交偏移量,用 Runtime.addShutdownHook(第 6.3 节)或者显式调用 close(),不要指望 daemon 线程的 finally。

守护线程适合什么?适合「丢了也无所谓」的辅助工作:本地缓存清理、监控心跳、调试用的采样。判断标准很简单:如果 JVM 退出时这项工作没做完,会不会造成数据不一致或资源泄漏?会,就不能用 daemon。

还有一个继承规则:新线程的 daemon 属性默认继承创建它的线程。主线程是非守护的,所以 new Thread(...) 默认也是非守护。在线程池里创建的线程继承的是池里工作线程的属性,这会让「某个任务偷偷创建 daemon 线程」的行为变得难以预测。统一在 ThreadFactory 里显式设置,是更稳的做法。

在 libraryd-conc 里落地

借阅服务需要一个后台清理器,定期扫过期预约。它适合做成守护线程,因为清理延迟一秒不会造成数据错误;但必须提供显式的 close(),让测试和优雅停机可以主动收尾:

package com.plume.conc.core;

public final class ExpirySweeper implements AutoCloseable {

    private final Thread worker;
    private volatile boolean running = true;

    public ExpirySweeper() {
        this.worker = Thread.ofPlatform()
                .name("loan-expiry-sweeper")
                .daemon(true)
                .start(this::loop);
    }

    private void loop() {
        while (running) {
            try {
                Thread.sleep(1_000);
                sweepExpiredReservations();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return;
            }
        }
    }

    private void sweepExpiredReservations() {
        // 真实实现会查 LoanRepository,这里只保留调用边界
        if (Thread.currentThread().isInterrupted()) {
            return;
        }
    }

    @Override
    public void close() {
        running = false;
        worker.interrupt();
        try {
            worker.join(2_000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println("sweeper state=" + worker.getState());
    }
}

三个设计点:

  • running 用 volatile,因为它在主线程写、清理线程读。第 4 章会解释为什么这里不加 volatile 可能永远停不下来。
  • close() 先改标志再 interrupt(),让睡眠中的线程立刻醒来;只改标志的话,最坏要等一个完整的 1 秒周期。
  • join(2_000) 给收尾设上限,避免关闭流程被一个卡住的清理任务拖死。

注意 close() 里的 System.out.println 只是演示。生产代码应该把状态交给日志和指标,而不是标准输出;也不要在 close() 里做可能阻塞的远程调用。

常见错误与边界

错误后果改法
轮询 getState() 等线程结束忙等、读到瞬时值、可能永远等不到用 join() 或 CountDownLatch
用 BLOCKED 判断 ReentrantLock 竞争实际是 WAITING,方向判断错看栈顶是 park 还是监视器入口
在 daemon 线程的 finally 里刷盘JVM 退出时 finally 不执行,数据丢失用 shutdown hook 或显式 close()
start() 之后再 setDaemon(true)抛 IllegalThreadStateException在 start() 前设置,或用 builder
用 setPriority(10) 保证关键任务平台可能忽略,且不能插队用队列、限流、独立线程池隔离
认为 RUNNABLE 就是 CPU 满载IO 阻塞也可能显示 RUNNABLE结合 cpu= 时间与系统工具判断

什么时候不要用守护线程:任何涉及数据写入、事务提交、远程确认、锁释放的收尾工作。daemon 的语义是「可以随时被抛弃」,不是「优先级低的线程」。如果一项工作必须完成,它就应该有非守护线程或 shutdown hook 兜底。

小结

  • 六种状态里,BLOCKED 只对应 synchronized 等锁;ReentrantLock 等锁是 WAITING。
  • 持有锁并 sleep 的线程是 TIMED_WAITING,不是 BLOCKED;这是转储里最常见的一处误判。
  • RUNNABLE 包含「等待 CPU」和部分 native 阻塞,不能直接等同于 CPU 繁忙。
  • 优先级只是平台相关的提示,不能用来保证顺序或及时性。
  • 守护线程不阻止 JVM 退出,finally 不保证执行;必须完成的收尾要放在 shutdown hook 或显式 close() 里。

阅读导航:上一节 1.2 Thread、Runnable、Callable 与任务抽象 | 下一节 2.1 竞态的三种形态

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《Java 消息中间件·事件驱动架构》附录 B 架构决策记录模板
  2. 《Java 消息中间件·事件驱动架构》附录 A 术语表
  3. 《Java 消息中间件·事件驱动架构》15.3 全书验收