跳到主要内容
约 7 分钟3 张卡

Java 线程

Java 线程是执行任务的基本单元,从创建到终止具有明确的状态。本文先比较 start() 与 run(),再介绍六种线程状态,最后说明中断如何传递以及阻塞方法怎样响应。中断是协作请求,任务何时退出取决于执行中的代码和调用的 API。

1. 面试核心​

先记住这几条,再读正文
  • 启动与直接调用。 start() 启动新线程,直接调用平台线程的 run() 则在当前线程执行任务。
  • 线程状态。 线程状态为 NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING 和 TERMINATED。
  • 中断查询。 Thread.interrupted() 读取并清除当前线程的中断标志。卡 · 中断状态的读取与清除
  • 中断异常。 sleep、wait、join 和 JUC 可中断阻塞方法响应中断时,会清除标志并抛出 InterruptedException。
  • 协作退出。 任务通过检查中断或调用可中断 API 协作退出,并在退出前释放资源。卡 · 协作式中断

2. start() 与 run()​

创建线程对象只是准备任务,调用 start() 才会启动新线程。下面的代码启动工作线程后,再等待它执行结束:

Thread worker = new Thread(() -> loadData());
worker.start();
worker.join();

本例创建的是平台线程,由操作系统线程承载执行。对象刚创建时处于 NEW 状态。调用 start() 后,新线程进入调度,调用者可以与它并发运行;随后调用 join(),调用者就等待工作线程结束。

若把 start() 换成直接调用 worker.run(),loadData() 就在当前线程中执行。这只是普通的方法调用,执行完后当前线程才继续下一行。

一个 Thread 只可启动一次,重复 start() 会抛出 IllegalThreadStateException。run() 正常返回,或抛出的未捕获异常处理结束后,线程进入 TERMINATED。这时若还需要执行任务,应创建新线程或交给线程池。

Java 21 支持由 JVM 调度的虚拟线程,它同样通过启动进入执行。直接调用虚拟线程对象的 run() 没有效果。

虚拟线程的调度方式见 虚拟线程。

3. 线程的六种状态​

Thread.State 描述线程在 JVM 中的执行或等待状态。理解这六种状态,有助于判断线程是在等锁、等条件,还是已经结束:

状态含义
NEW尚未启动
RUNNABLE可运行或正在执行,也包含部分系统资源等待
BLOCKED等待进入或重新进入 monitor
WAITING无期限等待,如 wait()、join()、park()
TIMED_WAITING有期限等待,如 sleep() 或定时等待
TERMINATED执行已结束

这些状态属于 JVM 层,与操作系统线程状态并非一一对应。比如,RUNNABLE 既可以表示正在运行或等待调度,也可包含 native I/O 等待。native I/O 指通过本地系统调用执行的输入输出,所以看到 RUNNABLE 时,还要确认线程是否正在消耗 CPU。

诊断时应结合以下三类信息:

  • 线程栈显示线程当前执行或等待的方法。
  • 相关队列显示任务是否积压、线程在等什么工作。
  • CPU 使用情况帮助区分计算繁忙与等待系统资源。

4. 协作式中断​

interrupt() 向目标线程发出中断请求,由任务决定如何结束工作。普通计算需要主动检查中断标志,才能在合适的位置退出:

while (!Thread.currentThread().isInterrupted()) {
processNextBatch();
}

示例在每批工作开始前检查标志。当前批次执行中收到中断时,会先执行完这一批,再在下一次检查时退出。因此,批次越长,任务响应取消的时间就越长。

isInterrupted() 查询目标线程的标志,并保留原值,适合上面的循环检查。静态方法 Thread.interrupted() 则查询当前线程,并清除标志。它适合需要读取并处理该中断的代码,普通查询应留意这个清除动作。

线程正阻塞在某个 API 中时,是否返回或抛出异常由该 API 决定。下一节说明常见可中断阻塞方法的行为。

5. InterruptedException 的传播​

sleep、wait、join 以及 BlockingQueue.take() 等方法会通过异常响应中断。它们抛出 InterruptedException 前会清除中断标志,所以调用方需要继续传递这个取消请求。

处理方式取决于当前方法能否声明受检异常。可以继续声明时,直接向上抛出:

public Result load() throws InterruptedException {
return queue.take(); // 继续向上抛出
}

这段代码让调用方通过异常知道等待已经被中断。若当前接口无法继续声明 InterruptedException,可以恢复标志并转换为业务异常:

try {
return queue.take();
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
throw new TaskCancelledException(exception);
}

第二段假设业务定义了 TaskCancelledException。恢复标志后,上层除了捕获业务异常,还能通过线程状态检测到中断。若捕获后只记录日志并继续执行,异常和标志都被消耗,上层的关闭或取消请求就会丢失。

6. 安全终止线程​

安全终止要让任务在状态一致的位置退出,并释放已经取得的资源。任务可以检查中断,也可以检查用 volatile 或锁保护的停止标志,确保修改对工作线程可见。单独修改停止标志时,已经阻塞的调用还需通过中断、关闭或超时结束等待。

任务在 finally 或资源作用域内完成清理,例如解锁、归还连接和关闭资源。调用方按以下顺序完成协作关闭:

  1. 停止接收新任务,避免关闭期间继续增加工作。
  2. 发出中断请求,或设置受同步保护的停止条件。
  3. 在期限内等待已有任务退出,再处理仍未完成的任务。

关闭流程和资源清理的完整说明见 任务取消。

Java 17 的 Thread.stop() 会强制终止线程并释放它持有的 monitor。若线程刚改完一组字段中的一部分,其他线程就会读到不一致状态,因此该方法已弃用。Java 21 中,该方法始终抛出 UnsupportedOperationException。

线程池通过 ExecutorService.shutdown() 开始有序关闭,超时后可按业务需要调用 shutdownNow() 尝试取消。后者尝试中断运行中的任务,并返回尚未开始的任务,实际退出仍依赖任务响应。

排队任务的处理见 Java 线程池。

7. 中断状态的读取与清除​

区分查询方法时,要同时看它查询哪个线程、是否清除标志。下面先中断当前线程,再连续读取三次:

Thread.currentThread().interrupt();
System.out.println(Thread.currentThread().isInterrupted());
System.out.println(Thread.interrupted());
System.out.println(Thread.interrupted());

没有额外中断时,输出依次为 true、true、false。第一次 isInterrupted() 读到标志后保留它,所以接下来的 Thread.interrupted() 仍返回 true。但这次静态调用同时清除了标志,因此第三次读取返回 false。

8. park 与中断返回​

LockSupport.park() 用来暂停当前线程,其响应中断的方式与 sleep 不同。它检测到中断时直接返回,并保留中断标志。它也允许在没有有效唤醒时返回,所以调用者要在循环中重新检查等待条件和中断状态。

若循环忽略已经设置的中断标志,下一次 park 仍会立即返回。这样循环持续执行,却没有真正等待,形成空转并消耗 CPU。相比之下,sleep 响应中断时会抛出异常并清除标志,调用方需要在异常路径处理取消。

许可与先唤醒机制见 AQS。

9. 常见问题​

9.1 sleep 和 wait 都会释放锁吗?​

sleep 会保留当前线程持有的 monitor,wait 会释放目标对象的 monitor。sleep 是 Thread 提供的定时暂停,wait 则用于对象上的条件等待。调用 wait 前必须持有目标 monitor,返回前也要重新获取它。

条件等待的完整机制见 synchronized。

9.2 中断一定能结束网络 I/O 吗?​

10 · 面试题卡片​

10.1 Java 中怎样正确停止一个线程​

出现公司:拼多多、美团

考察重点

  • 说明任务为什么需要主动响应中断请求。
  • 区分计算循环和阻塞 API 响应中断的方式。
  • 解释退出前怎样清理资源,以及调用方怎样限制等待时间。

相关内容:协作式中断至安全终止线程。

参考回答

调用方通过 interrupt() 发出请求,任务响应后在状态一致的位置协作退出。

计算循环用 isInterrupted() 检查,阻塞等待则选择可中断 API。捕获 InterruptedException 后继续抛出,或恢复中断标志并退出当前任务。

任务在 finally 或资源作用域中完成清理,调用方在期限内等待结束。线程池先调用 shutdown(),等待超时后再按业务需要调用 shutdownNow()。中断只发出请求,任务仍需执行取消逻辑。

已经发生的外部操作需要查询结果,重试时用幂等规则避免重复执行,必要时进行补偿。

10.2 isInterrupted 和 Thread.interrupted 有什么区别?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:说明两个方法分别查询哪个线程,以及连续查询时清除标志会怎样影响结果。

相关内容:中断状态的读取与清除。

参考回答

isInterrupted() 查询目标线程并保留标志,Thread.interrupted() 查询当前线程并清除标志。

若当前线程已被中断,而且没有新的中断请求,连续两次静态查询会依次得到 true 和 false。原因是第一次调用已经把标志清除了。

10.3 线程中断后,为什么 park 循环会持续空转?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:解释保留中断标志为什么会让后续 park() 立即返回,以及循环应如何处理取消。

相关内容:park 与中断返回。

参考回答

park() 响应中断后保留标志,循环若忽略它,后续调用就会持续立即返回。

循环因此一直执行、消耗 CPU,而没有真正等待。任务应按取消协议退出,或由明确的协议处理这个中断,再决定是否继续等待。

11. 参考资料​