Java 线程详解:从创建、协作、生命周期到中断机制
欢迎你来读这篇博客,这篇博客主要是关于 Java 线程。线程是 Java 并发编程的入口,也是很多线上问题的起点:接口偶发超时、任务卡死、线程池打满、死锁、异步任务无法退出、服务关闭很慢,这些问题背后基本都绕不开线程。
如果用一句话概括:线程是程序中一条独立的执行路径,而 Java 提供了一套从线程创建、调度、协作、等待、唤醒到取消的完整机制。真正写好并发代码,不只是会 new Thread(),更重要的是理解线程的生命周期、协作边界和中断语义。
一、线程的介绍 Thread Overview
1. 什么是线程
在操作系统里,进程是资源分配的基本单位,线程是 CPU 调度的基本单位。一个 Java 应用启动后,JVM 本身就是一个进程,而这个进程内部可以同时运行多个线程。
Oracle JDK 文档对 Thread 的定义很直接:线程是程序中的一条执行线程,JVM 允许一个应用中有多条执行线程并发运行。换成更工程化的话说:
- 一个进程可以包含多个线程。
- 多个线程共享同一个进程的堆内存、方法区、打开的文件、网络连接等资源。
- 每个线程又有自己的程序计数器、虚拟机栈和本地方法栈。
- 多线程可以提升吞吐量,但也会引入线程安全、锁竞争、可见性、死锁、上下文切换等问题。
flowchart LR
A[Java 进程 / JVM] --> B[主线程 main]
A --> C[GC 线程]
A --> D[业务线程 worker-1]
A --> E[业务线程 worker-2]
A --> F[监控/调度线程]
D --> G[执行任务 A]
E --> H[执行任务 B]
2. 并发和并行不是一回事
线程经常和两个概念一起出现:并发 和 并行。
并发强调的是“同时处理多个任务”的能力。它不要求多个任务在同一瞬间真的一起运行,单核 CPU 也可以通过时间片切换实现并发。
并行强调的是“多个任务在同一瞬间真的一起执行”。它通常依赖多核 CPU 或多台机器。
简单说:
- 并发:一段时间内处理多个任务。
- 并行:同一时刻执行多个任务。
Java 多线程既可以用于并发,也可以用于并行。比如 Web 服务同时处理很多请求,这是并发;多个 CPU 核心同时跑计算任务,这是并行。
3. CPU 密集型和 I/O 密集型
线程数量怎么设置,要先看任务类型。
CPU 密集型任务主要消耗 CPU,例如加密、压缩、图像处理、大量数学计算。这类任务线程数通常不宜远超 CPU 核数,否则上下文切换会抵消收益。
I/O 密集型任务主要等待外部资源,例如数据库、Redis、HTTP、文件、消息队列。这类任务经常处于等待状态,可以配置更多线程提高吞吐量。
这也是为什么 JDK 21 引入正式版虚拟线程后,很多服务端应用会重新讨论“一个请求一个线程”的模型。虚拟线程很适合大量 I/O 阻塞任务,但它不是让 CPU 计算变快的魔法。线程不是许愿池,扔进去太多任务也不会自动变成性能。
4. 平台线程和虚拟线程
Java 传统线程通常称为 平台线程。平台线程一般和操作系统内核线程一一映射,由操作系统调度,创建和切换成本相对较高。
JDK 21 正式引入 虚拟线程。虚拟线程由 JDK 管理,通常挂载在少量平台线程之上,适合大量等待 I/O 的任务。
flowchart TD
A[Java Thread] --> B[平台线程 Platform Thread]
A --> C[虚拟线程 Virtual Thread]
B --> D[通常映射到 OS 线程]
B --> E[适合各种任务,但资源较重]
C --> F[由 JDK 调度]
C --> G[适合大量阻塞 I/O]
C --> H[不适合长时间 CPU 密集计算]
需要注意的是,虚拟线程并没有改变 Java 的基本并发模型。Thread、Runnable、Callable、ExecutorService、ThreadLocal、synchronized、Lock 这些概念仍然重要,只是线程的成本模型变了。
二、线程的创建 Thread Creation
Java 创建线程的方式很多,但核心思路只有一个:把一段要执行的任务交给线程运行。
1. 继承 Thread
最直接的方式是继承 Thread 并重写 run() 方法。
1 | |
这种方式很直观,但不推荐作为常规写法。原因是 Java 单继承,继承 Thread 会把“任务逻辑”和“线程对象”绑死,扩展性较差。
2. 实现 Runnable
更常见的方式是实现 Runnable,把任务和线程分离。
1 | |
这种写法的核心优势是:Runnable 表示任务,Thread 表示执行任务的线程。任务可以交给普通线程,也可以交给线程池。
3. Callable + FutureTask
Runnable 没有返回值,也不能直接抛出受检异常。如果需要返回结果,可以使用 Callable。
1 | |
FutureTask#get() 会阻塞当前线程,直到任务执行完成或抛出异常。因此在业务代码里使用 get() 时要小心,不要在关键线程上无脑阻塞。
4. 使用 ExecutorService
在实际项目里,一般不建议频繁手动创建线程,而是使用 ExecutorService 管理线程。
1 | |
线程池的意义不只是“复用线程”,还包括:
- 限制并发量,避免无限创建线程。
- 统一管理线程生命周期。
- 统一命名线程,方便排查问题。
- 配合队列、拒绝策略、监控指标做容量治理。
不过 Executors.newFixedThreadPool()、Executors.newCachedThreadPool() 这些工厂方法在生产环境要谨慎使用,因为默认队列或线程数量可能不符合业务容量边界。更稳妥的方式是直接使用 ThreadPoolExecutor,显式配置核心线程数、最大线程数、队列长度、线程工厂和拒绝策略。
5. 使用 Thread.Builder
较新的 JDK 提供了 Thread.Builder,可以更清晰地创建平台线程或虚拟线程。
1 | |
虚拟线程写法如下:
1 | |
或者使用虚拟线程版 ExecutorService:
1 | |
6. start() 和 run() 的区别
这是 Java 线程里最经典的问题。
start() 的作用是启动一个新线程,由 JVM 调用该线程的 run() 方法。调用后,会出现两条并发执行路径:当前线程继续往下走,新线程执行 run()。
run() 只是一个普通方法。直接调用 run() 不会创建新线程,而是在当前线程里同步执行。
1 | |
还有一个重要规则:同一个 Thread 对象只能 start() 一次。线程执行结束后不能重新启动,重复调用会抛出 IllegalThreadStateException。
三、线程的协作 Thread Coordination
线程不是创建出来就完事了。真正复杂的是多个线程之间如何等待、通知、共享结果、控制顺序和安全退出。
1. join:等待另一个线程结束
join() 用于让当前线程等待另一个线程执行结束。
1 | |
join() 常用于“主线程等待子线程完成”的场景。比如启动多个任务后,主线程要等它们都完成再汇总结果。
注意:join() 会响应中断。如果当前线程在等待期间被中断,会抛出 InterruptedException。
2. sleep:暂停当前线程
Thread.sleep() 会让当前线程暂停一段时间,进入 TIMED_WAITING 状态。
1 | |
几个关键点:
sleep()暂停的是当前线程。sleep()不会释放已经持有的对象锁。sleep()的时间不保证绝对精确,受操作系统调度影响。sleep()可以被中断,被中断后抛出InterruptedException。
3. wait / notify / notifyAll
wait()、notify()、notifyAll() 是 Object 上的方法,不是 Thread 上的方法。原因是每个 Java 对象都可以作为监视器锁,每个对象也都有对应的等待集合。
典型写法如下:
1 | |
这里最重要的是:wait() 必须放在 while 循环里,而不是 if。
原因有三个:
- 线程可能被虚假唤醒。
- 线程被唤醒后,条件可能已经被其他线程改变。
notify()只能说明“可能有事发生”,不能保证等待条件一定成立。
wait() 和 notifyAll() 还必须在持有同一个对象监视器锁的前提下调用,也就是通常要放在 synchronized 代码块或同步方法里。否则会抛出 IllegalMonitorStateException。
1 | |
对应的通知:
1 | |
4. wait 和 sleep 的区别
wait() 和 sleep() 都会让线程暂停,但语义完全不同。
| 对比项 | wait | sleep |
|---|---|---|
| 所属类 | Object |
Thread |
| 是否需要锁 | 必须持有对象监视器锁 | 不需要 |
| 是否释放锁 | 会释放当前对象锁 | 不释放锁 |
| 唤醒方式 | notify、notifyAll、中断、超时、虚假唤醒 |
时间到或中断 |
| 典型用途 | 线程协作、条件等待 | 暂停、限速、简单等待 |
5. Lock / Condition
synchronized + wait/notify 是 Java 内置协作机制。更复杂的场景可以使用 Lock + Condition。
1 | |
Condition 的好处是可以为一把锁创建多个等待队列,表达能力比单一对象监视器更强。
6. 常用并发协作工具
实际项目里,如果只是为了完成线程协作,优先考虑 java.util.concurrent 包里的高级工具,而不是手写复杂的 wait/notify。
| 工具 | 作用 | 常见场景 |
|---|---|---|
CountDownLatch |
一个或多个线程等待其他线程完成 | 主线程等多个初始化任务完成 |
CyclicBarrier |
多个线程互相等待,到齐后继续 | 多阶段并行计算 |
Semaphore |
控制同时访问资源的线程数 | 限流、连接数控制 |
BlockingQueue |
阻塞队列 | 生产者消费者 |
CompletableFuture |
异步编排 | 多接口并行调用、结果组合 |
ExecutorService |
管理任务执行 | 线程池、异步任务 |
生产者消费者可以用 BlockingQueue 写得很干净:
1 | |
sequenceDiagram
participant P as Producer
participant Q as BlockingQueue
participant C as Consumer
P->>Q: put(data)
alt queue full
P-->>P: block
end
C->>Q: take()
alt queue empty
C-->>C: block
end
Q-->>C: data
四、线程的生命周期 Thread Lifecycle
Java 线程的生命周期可以通过 Thread.State 观察。官方定义里有 6 种状态:
NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED
stateDiagram-v2
[*] --> NEW: new Thread()
NEW --> RUNNABLE: start()
RUNNABLE --> BLOCKED: waiting for monitor lock
BLOCKED --> RUNNABLE: acquired monitor lock
RUNNABLE --> WAITING: wait()/join()/park()
WAITING --> RUNNABLE: notify/notifyAll/unpark/interrupt
RUNNABLE --> TIMED_WAITING: sleep()/wait(timeout)/join(timeout)
TIMED_WAITING --> RUNNABLE: timeout/notify/interrupt
RUNNABLE --> TERMINATED: run() completed
TERMINATED --> [*]
1. NEW
创建了 Thread 对象,但还没有调用 start()。
1 | |
2. RUNNABLE
调用 start() 后,线程进入 RUNNABLE。
很多人会把 RUNNABLE 理解成“正在运行”,这不完全准确。在 Java 线程状态里,RUNNABLE 包括两种情况:
- 正在 CPU 上执行。
- 已经可以执行,但正在等待操作系统分配 CPU 时间片。
也就是说,Java 的 RUNNABLE 覆盖了操作系统层面的 ready 和 running。
3. BLOCKED
线程等待进入 synchronized 同步代码块或同步方法时,会进入 BLOCKED。
1 | |
这里 t2 不是在等 sleep(),而是在等 LOCK 的监视器锁。
4. WAITING
线程无限期等待另一个线程执行某个动作时,会进入 WAITING。
常见方式:
Object.wait()无超时参数。Thread.join()无超时参数。LockSupport.park()。
5. TIMED_WAITING
线程带超时时间等待时,会进入 TIMED_WAITING。
常见方式:
Thread.sleep(timeout)。Object.wait(timeout)。Thread.join(timeout)。LockSupport.parkNanos()。LockSupport.parkUntil()。
6. TERMINATED
线程执行完成后进入 TERMINATED。完成方式可以是:
run()正常执行结束。run()抛出未捕获异常后结束。
线程一旦结束,就不能重新启动。
7. 如何排查线程状态
线上排查线程问题时,经常会用到:
1 | |
常见判断思路:
- 大量线程
BLOCKED:可能存在锁竞争或死锁。 - 大量线程
WAITING:可能在等待队列、锁条件、任务结果。 - 大量线程
TIMED_WAITING:可能在 sleep、定时等待、连接池等待。 - 业务线程数量持续上涨:可能线程池配置不合理,或者任务无法结束。
- CPU 很高但线程都
RUNNABLE:可能有死循环、过度计算、频繁 GC 或锁自旋。
线程状态只是入口,不是结论。看到 WAITING 不一定有问题,看到 RUNNABLE 也不一定正常。并发问题最烦人的地方就在这里:它很少直接喊“我错了”,更多时候只是安静地把服务拖慢。
五、守护线程 Daemon Threads
1. 什么是守护线程
Java 线程分为两类:
- 用户线程,也叫非守护线程。
- 守护线程,也叫 daemon thread。
JVM 是否退出,主要看是否还有存活的非守护线程。当所有非守护线程都结束后,JVM 会开始关闭流程;守护线程不会阻止 JVM 退出。
典型守护线程包括一些后台辅助线程,例如 GC 相关线程、监控线程、清理线程等。
2. 如何创建守护线程
1 | |
当 main 线程结束后,如果没有其他非守护线程,JVM 就可以退出,守护线程不会继续强行维持进程。
注意:setDaemon(true) 必须在 start() 之前调用。线程启动后再设置会抛出 IllegalThreadStateException。
3. 守护线程适合做什么
守护线程适合做后台辅助性任务,例如:
- 指标上报。
- 缓存清理。
- 心跳检测。
- 临时监控。
- 非关键日志缓冲。
守护线程不适合做关键业务任务,例如:
- 支付扣款。
- 订单状态落库。
- 文件最终保存。
- MQ 关键消息投递。
- 需要保证完成的数据修复任务。
原因很简单:JVM 退出时,守护线程可能来不及执行 finally 逻辑,也不保证一定完成收尾动作。关键业务放守护线程里,就像把房本交给便利贴保管,形式上有地方放,实际上很刺激。
4. 虚拟线程和守护线程
JDK 文档中说明,虚拟线程是守护线程,因此虚拟线程不会阻止 JVM 退出。
这意味着如果你在 main 方法里启动一个虚拟线程,却没有 join()、Future#get() 或其他等待逻辑,主线程结束后程序可能直接退出。
1 | |
如果你希望看到虚拟线程结果,需要等待它完成:
1 | |
六、中断机制 Thread Interruption
1. 中断不是强制停止
Java 的中断机制很容易被误解。interrupt() 不是强制杀死线程,而是向线程发出一个协作式取消信号。
也就是说:
- 调用方:我希望你停一下。
- 被调用线程:我看到了,我决定怎么安全退出。
Java 不推荐粗暴停止线程,因为线程可能正持有锁、正在修改共享数据、正在写文件或正在处理事务。如果强行杀死,资源和数据可能处于不一致状态。
2. interrupt 做了什么
每个线程内部都有一个中断标记。
调用:
1 | |
通常会产生两种效果:
- 如果目标线程正在
sleep()、wait()、join()等可中断阻塞方法中,会抛出InterruptedException,并清除中断标记。 - 如果目标线程正在正常运行,通常只是把中断标记设置为
true,线程需要自己检查这个标记并退出。
3. isInterrupted 和 interrupted 的区别
1 | |
两者很像,但区别非常关键。
| 方法 | 作用对象 | 是否清除中断标记 |
|---|---|---|
thread.isInterrupted() |
指定线程 | 不清除 |
Thread.interrupted() |
当前线程 | 清除 |
示例:
1 | |
如果你只是想判断当前线程有没有被中断,并且不想清除标记,优先用:
1 | |
4. 正确处理中断
处理中断有一个非常实用的规则:能抛就抛,不能抛就恢复中断标记。
如果方法允许抛出 InterruptedException,可以直接向上抛。
1 | |
如果当前方法不能抛出受检异常,就捕获后恢复中断标记。
1 | |
不要这样写:
1 | |
这叫吞掉中断。它会让上层代码以为线程没有被取消,结果任务继续跑,服务关闭卡住,线程池迟迟不退出,最后大家一起看日志发呆。
5. 循环任务如何响应中断
对于长期运行的任务,要在循环中定期检查中断标记。
1 | |
如果循环中包含阻塞方法,也要正确处理 InterruptedException。
1 | |
6. 线程池关闭和中断
ExecutorService#shutdown() 是有序关闭:不再接收新任务,已经提交的任务继续执行。
ExecutorService#shutdownNow() 会尝试停止正在执行的任务,并返回尚未开始执行的任务列表。注意这里是“尝试”,典型实现方式是通过 Thread.interrupt() 发出中断信号。如果任务不响应中断,它仍然可能不退出。
推荐关闭流程:
1 | |
这个模板的关键点:
- 先
shutdown(),给任务正常完成机会。 - 超时后
shutdownNow(),尝试中断任务。 - 当前关闭线程如果被中断,要恢复中断标记。
七、常见问题和最佳实践
1. 不要在生产代码里随意 new Thread
手动创建线程适合 demo、简单工具、少量后台任务。业务系统里更推荐线程池或虚拟线程执行器。
原因很现实:
- 线程数量不可控。
- 线程名称不统一。
- 异常处理分散。
- 无法统一监控。
- 服务关闭时难以管理。
2. 给线程命名
线程名是排查线上问题的第一入口。
1 | |
线程池可以使用自定义 ThreadFactory:
1 | |
线上看到 pool-7-thread-13,只能知道它是线程。看到 settlement-export-3,至少知道该去找哪个业务模块。
3. 优先使用高级并发工具
不要一上来就手写 wait/notify。能用 BlockingQueue、CountDownLatch、CompletableFuture、Semaphore、ExecutorService 的地方,优先使用这些经过充分验证的工具。
手写底层并发控制不是不行,但要非常清楚条件谓词、锁范围、异常路径、中断恢复和内存可见性。并发代码最擅长让“看起来没问题”的代码变成“偶尔有问题”的代码。
4. 共享变量要考虑可见性和原子性
多个线程访问共享变量时,要考虑三个问题:
- 原子性:操作是否不可分割。
- 可见性:一个线程修改后,另一个线程是否能及时看到。
- 有序性:代码执行顺序是否可能被编译器或 CPU 优化重排。
常用手段:
synchronizedvolatileLockAtomicInteger、AtomicLong等原子类- 并发容器,例如
ConcurrentHashMap
例如停止标记可以用 volatile:
1 | |
但 volatile 不能保证复合操作的原子性。下面这种自增不是线程安全的:
1 | |
因为 count++ 包含读取、加一、写回三个步骤。要用原子类或锁。
5. 不要使用 stop、suspend、resume
Thread.stop()、Thread.suspend()、Thread.resume() 这些方法早已不推荐使用。
原因是它们太粗暴:
stop()可能破坏对象状态一致性。suspend()可能在持有锁时挂起线程,导致死锁。resume()依赖外部恢复,一旦调用顺序错误,问题很难排查。
正确方向是使用协作式取消:中断、状态标记、关闭队列、关闭线程池。
6. 不要把锁范围写得太大
锁保护的是共享状态,不是整段业务逻辑。锁范围越大,线程竞争越严重。
不推荐:
1 | |
更好的方向是缩小同步范围:
1 | |
特别是远程调用、文件 I/O、数据库访问,不要随便放在大锁里。
7. 虚拟线程使用建议
虚拟线程适合:
- 大量阻塞 I/O。
- 一个请求一个任务。
- HTTP/RPC/DB 调用较多的服务端业务。
- 希望简化异步回调代码的场景。
虚拟线程不适合:
- 长时间 CPU 密集计算。
- 依赖固定线程数限流的场景。
- 大量使用
ThreadLocal存放重对象的场景。 - 在旧版 JDK 中长时间持有
synchronized并执行阻塞操作的场景。
使用虚拟线程时,限流不要靠“线程池大小”,而要用业务资源本身的限制,例如连接池、Semaphore、队列容量、接口配额。
八、一张表总结
| 主题 | 核心结论 |
|---|---|
| 线程是什么 | 程序中的独立执行路径,JVM 支持多线程并发执行 |
| 创建线程 | 推荐任务和线程分离,常用 Runnable、Callable、ExecutorService |
| 启动线程 | 调用 start() 才会创建新执行路径,直接调 run() 只是普通方法调用 |
| 线程协作 | 简单等待用 join,条件等待用 wait/notify 或 Condition,复杂协作用 java.util.concurrent |
| 生命周期 | Java 线程有 NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED 六种状态 |
| 守护线程 | 不阻止 JVM 退出,适合后台辅助任务,不适合关键业务 |
| 中断机制 | 中断是协作式取消,不是强制杀死线程 |
| 最佳实践 | 线程命名、统一线程池、正确处理中断、缩小锁范围、优先使用高级并发工具 |
九、面试常见追问
1. Java 线程有几种状态?
6 种:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。
2. BLOCKED 和 WAITING 有什么区别?
BLOCKED 通常是等 synchronized 的监视器锁。
WAITING 是等待另一个线程执行某个动作,比如 wait() 等待通知,join() 等待线程结束。
3. wait 为什么要写在 while 里?
因为线程可能虚假唤醒,被唤醒后条件也可能已经被其他线程改变。notify 只能说明“可能有变化”,不能保证条件成立。
4. interrupt 能不能停止线程?
不能强制停止。interrupt() 只是发出中断信号。线程要么在阻塞方法中收到 InterruptedException,要么主动检查中断标记并退出。
5. 捕获 InterruptedException 后为什么要重新 interrupt?
因为抛出 InterruptedException 时通常会清除中断标记。如果当前方法不能继续向外抛异常,就应该调用 Thread.currentThread().interrupt() 恢复中断状态,让上层代码知道发生过中断。
6. 守护线程会不会执行 finally?
不能依赖。JVM 退出时,守护线程可能没有机会完整执行收尾逻辑。因此关键资源释放、数据落库、消息投递不要依赖守护线程。
十、结语
Java 线程这块知识,看起来是基础,实际非常考验工程判断。
初学时,我们关注的是“怎么创建线程”;工作后,更应该关注“线程如何协作、如何退出、如何排查、如何限制资源、如何避免共享状态失控”。线程本身不是问题,失控的线程才是问题。
比较稳的实践路线是:
- 普通业务异步任务:优先使用线程池。
- 大量阻塞 I/O:考虑 JDK 21+ 虚拟线程。
- 线程间通信:优先使用并发工具类。
- 停止任务:使用中断和协作式取消。
- 排查问题:结合线程名、线程状态、堆栈和业务日志。
把这些原则吃透之后,再看线程池、锁、AQS、CompletableFuture、ForkJoinPool、虚拟线程、结构化并发,整个 Java 并发体系就会顺很多。
参考资料
- Oracle Java SE 25 API:
java.lang.Thread
https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Thread.html - Oracle Java SE 21 API:
java.lang.Object
https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Object.html - Oracle Java SE 25 API:
Thread.State
https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Thread.State.html - Oracle Java Tutorials: Interrupts
https://docs.oracle.com/javase/tutorial/essential/concurrency/interrupt.html - Oracle Java Tutorials: Joins
https://docs.oracle.com/javase/tutorial/essential/concurrency/join.html - Oracle Java Tutorials: Guarded Blocks
https://docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html - Oracle Java Tutorials: Pausing Execution with Sleep
https://docs.oracle.com/javase/tutorial/essential/concurrency/sleep.html - Oracle Java Tutorials: Lock Objects
https://docs.oracle.com/javase/tutorial/essential/concurrency/newlocks.html - Oracle Java SE API:
ExecutorService
https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html - OpenJDK JEP 444: Virtual Threads
https://openjdk.org/jeps/444 - Java Language Specification SE 21, Chapter 17: Threads and Locks
https://docs.oracle.com/javase/specs/jls/se21/html/jls-17.html