面试考察点

  • 是否掌握 Java 的六种线程状态及转换条件。
  • 能否区分 waitsleepjoin 与中断。
  • 是否会用条件循环避免虚假唤醒。

核心答案

Java 线程有 NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING 和 TERMINATED 六种状态。线程协作可以使用监视器的 wait/notify、Lock 的 Condition、join、中断标记以及并发工具类;生产代码通常优先高层并发组件。

Java 的 RUNNABLE 同时包含操作系统层面的就绪和运行。BLOCKED 表示等待进入 synchronized,WAITING 则可能在等待通知、join 或同步器信号。

关键机制

wait 必须在持有对应监视器时调用,并会释放该锁;sleep 不要求持锁,也不会主动释放已持有的锁。notify 只唤醒一个等待者,notifyAll 唤醒全部等待者重新竞争。

六种状态如何区分

状态 含义 常见触发
NEW 已创建,尚未启动 new Thread(...)
RUNNABLE 可以被 JVM 调度运行 执行计算、等待 OS 调度、部分本地 I/O
BLOCKED 等待进入监视器锁 竞争 synchronized
WAITING 无限期等待其他线程动作 Object.wait()Thread.join()LockSupport.park()
TIMED_WAITING 有时限等待 sleep、带超时 wait/join/park
TERMINATED run 方法结束 正常返回或未捕获异常退出

JVM 的状态不是操作系统线程状态的逐字映射。例如 Java 的 RUNNABLE 可能实际正在运行,也可能已就绪等待 CPU;排查时要结合线程栈、CPU、锁拥有者和 I/O 指标,而不能仅凭状态下结论。

正确的条件等待代码

class BoundedBuffer<T> {
    private final Queue<T> queue = new ArrayDeque<>();
    private final int capacity;

    synchronized void put(T item) throws InterruptedException {
        while (queue.size() == capacity) {
            wait();
        }
        queue.add(item);
        notifyAll();
    }

    synchronized T take() throws InterruptedException {
        while (queue.isEmpty()) {
            wait();
        }
        T item = queue.remove();
        notifyAll();
        return item;
    }
}

必须使用 while 而不是 if:线程可能虚假唤醒,也可能在被 notifyAll 唤醒后,条件已被其他竞争者再次改变。notifyAll 虽会造成额外竞争,但在存在多个不同等待条件时更不容易发生“唤醒了错误线程后所有人继续等待”的死锁。

wait、sleep、join、park 对比

API 是否释放已持有监视器 需要什么前提 主要用途
wait 持有对应对象监视器 等待业务条件变化
sleep 暂停当前线程一段时间
join 调用方等待目标线程结束 目标线程对象 等待线程生命周期结束
park 不强制关联监视器 同步器底层阻塞与许可机制

不要用 Thread.sleep 轮询另一个线程是否完成,既浪费延迟又难以及时取消;可用 Future、CountDownLatch、CompletableFuture 或条件变量表达明确依赖。

中断的正确处理

中断不是强制杀线程,而是一面协作标记。阻塞在 wait、sleep、join 等可中断方法上的线程会收到 InterruptedException,并清除中断标记;捕获后应继续抛出,或在无法抛出的接口中恢复:

catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    return;
}

吞掉中断会让线程池关闭、超时取消和应用停机无法及时生效。CPU 密集循环也要主动检查 isInterrupted() 并尽快退出。

线上排查路径

线程池请求堆积时,先看活跃线程是否 BLOCKED 在同一锁、WAITING 在队列、TIMED_WAITING 在不合理 sleep,还是 RUNNABLE 但 CPU 已满。连续抓取多份线程转储比单份更有价值:同一栈帧长时间不变才说明真正卡住。

正确协作

等待条件要写成 while (!condition) wait(),被唤醒后重新检查条件。中断是协作式取消信号,捕获 InterruptedException 后应向上抛出或恢复中断状态,不能静默吞掉。

常见误区

调用 start() 才会启动新线程,直接调用 run() 只是当前线程的普通方法调用。notify 也不会立即释放锁,只有同步块退出后等待线程才可能继续。

高频追问与参考回答

追问:为什么更推荐 BlockingQueue?

它把条件等待、唤醒、容量和可见性封装在明确 API 中,比手写 wait/notify 更不容易遗漏边界,并能自然表达生产消费与背压。

追问:调用 interrupt 后线程一定立即停止吗?

不一定。正在计算的线程只会设置中断标记,代码需要自行检查;可中断阻塞方法通常抛异常。无法响应中断的第三方调用还需要超时、隔离和进程级恢复策略。

追问:notify 能保证唤醒指定线程吗?

不能。它从该对象等待集里任选一个线程,调用方无法指定;有多个条件时应使用不同 Condition,或使用 notifyAll 后由每个线程重新判断条件。

追问:线程状态一直是 RUNNABLE 但接口很慢意味着什么?

可能是 CPU 密集循环、JNI/系统调用、忙等或线程在运行队列中等待 CPU。需结合 CPU 火焰图、系统负载和栈帧判断。

总结

回答时把状态、锁等待、条件等待和协作式中断串起来,并强调优先使用高层并发工具。

机制全景图

下面把「Java 线程有哪些状态?线程间如何通信?」从输入到结果压缩成一条可复述的主链路。面试时先用图建立全局坐标,再进入局部实现,能避免只背零散结论。

flowchart LR
    A["创建线程对象"]
    A --> B["start 进入 RUNNABLE"]
    B --> C["竞争锁或等待条件"]
    C --> D["被通知后重新竞争"]
    D --> E["完成任务进入 TERMINATED"]

完整链路:从输入到结果

沿着「创建线程对象 → start 进入 RUNNABLE → 竞争锁或等待条件 → 被通知后重新竞争 → 完成任务进入 TERMINATED」观察输入、状态与输出,下面每个阶段都对应一个可以在源码、日志或系统表中验证的位置。

1. 创建线程对象

NEW 线程尚未启动,同一 Thread 实例只能成功 start 一次。

2. start 进入 RUNNABLE

Java 的 RUNNABLE 同时涵盖操作系统可运行与正在运行,不能仅据此判断是否占用 CPU。

3. 竞争锁或等待条件

BLOCKED 等待 monitor,WAITING/TIMED_WAITING 常来自 wait、join、park、sleep 等不同机制。

4. 被通知后重新竞争

notify/unpark 只是让线程有资格继续,条件必须在循环中重新检查,且共享状态要受同一同步规则保护。

5. 完成任务进入 TERMINATED

run 正常返回或抛出未捕获异常后进入 TERMINATED,线程对象不能重启,应重新创建任务或交给执行器。

源码与实现定位

入口 阅读重点
java.lang.Thread.State 六种 JVM 线程状态
ObjectMonitor/LockSupport wait/notify 与 park/unpark 差异

源码或系统表应按上表顺序追踪:先确认入口实际走到哪条路径,再用运行时数据验证,而不是仅凭类名或配置推测。

参数配置与可复现实验

synchronized (queue) {
  while (queue.isEmpty()) queue.wait();
  task = queue.remove();
}

启动两个消费者和一个生产者,制造伪唤醒/竞争;用 jstack 区分 BLOCKED、WAITING 和 TIMED_WAITING。

验证步骤与预期结果

1. 固定输入和基线

先在没有故障注入的环境执行上述配置,固定数据规模、并发度、运行时版本和预热时间。以「等待线程数」为主基线,记录值应满足「与空闲消费者一致」;同时保存 各线程状态数量、等待持续时间,使后续变化能够回到同一时间轴比较。

2. 从实现入口确认路径

在「java.lang.Thread.State」确认请求确实进入「六种 JVM 线程状态」对应的实现,再沿「ObjectMonitor/LockSupport」观察「wait/notify 与 park/unpark 差异」。如果入口路径都未命中,就不应继续调整下游参数,而应先检查调用条件、版本或路由是否与假设一致。

3. 注入本文特有的失败模式

优先复现「用 if 代替 while 等待条件」,并把单一变量逐级放大,直到「等待线程数」越过「请求堆积仍 WAITING」。随后再分别验证「持锁 sleep 导致其他线程无法推进」和「混用 wait 与 park 却没有统一状态协议」,三类故障分开执行,避免多个变量同时变化而无法归因。

4. 执行止损和根因修复

第一轮只应用「if wait 改 while」,确认它能控制影响范围;第二轮应用「通知与状态修改放同一锁」,验证核心链路恢复;最后落实「优先 BlockingQueue/CountDownLatch」,消除同类问题再次出现的条件。每一步都保留变更前后数据,不用“感觉变快了”替代测量。

5. 通过退出条件

实验只有同时满足三项才算通过:「等待线程数」回到「与空闲消费者一致」、「BLOCKED P99」回到「接近 0」、「通知到运行」回到「受调度但稳定」,并且业务结果差异为零。若性能恢复但结果不一致,仍应视为失败;若指标恢复后很快再次越线,则说明只完成了临时止损,没有消除根因。

量化基线

指标 样例基线/口径 风险线 结论
等待线程数 与空闲消费者一致 请求堆积仍 WAITING 通知/条件错误
BLOCKED P99 接近 0 持续上升 锁竞争
通知到运行 受调度但稳定 长尾秒级 锁未释放/线程饥饿

这些数值是实验口径或示例告警线,不是可复制到所有系统的固定答案;上线阈值应由本系统稳态、峰值和故障演练共同确定。

事故复盘:消费者被通知后仍然读取空队列

代码用 if 判断队列为空后 wait,被唤醒时未重新检查条件,多个消费者竞争后其中一个拿走任务,另一个继续执行并失败。改为 while 条件循环,并在同一锁下修改队列和发送通知后才正确。

失败模式 首要证据 第一处置动作
用 if 代替 while 等待条件 各线程状态数量 if wait 改 while
持锁 sleep 导致其他线程无法推进 等待持续时间 通知与状态修改放同一锁
混用 wait 与 park 却没有统一状态协议 通知到真正运行延迟 优先 BlockingQueue/CountDownLatch

发布与回滚检查点

  • 发布前:确认「java.lang.Thread.State」对应实现和上述配置在目标版本仍然有效,并保存「等待线程数」基线。
  • 灰度中:同时观察 各线程状态数量、等待持续时间、通知到真正运行延迟;任一指标越过表中风险线,就停止继续扩量。
  • 回滚时:先执行「if wait 改 while」控制影响,再回退代码或参数;涉及持久状态时必须额外核对结果差异。
  • 发布后:至少覆盖一个完整峰值周期,确认「用 if 代替 while 等待条件」没有再次出现,才关闭变更观察窗口。

方案对比与选型

方案 更适合的场景 主要收益 代价与边界
wait/notify 同一 monitor 下的底层条件协作 无需额外类 易丢通知,单条件表达困难
Condition 显式锁且有多个等待条件 条件队列清晰、支持超时中断 必须严格持锁调用
BlockingQueue/同步器 标准生产消费或阶段协作 高层语义、安全成熟 需要选择容量与一致性语义

选型至少带上 并发线程数、临界区长度、阻塞比例和任务到达速率,并用上面的量化基线验证;未知数据应明确为待测假设。

设计边界与工程取舍

线程通信的核心不是“唤醒动作”,而是共享条件、状态修改与可见性的统一协议;能用高层并发工具时不要手写通知。

工程落地遵循:先建立 happens-before 与所有权边界,再谈吞吐和无锁优化。回答时直接引用「java.lang.Thread.State」、配置实验和事故数据,比复述固定模板更有说服力。