线程、进程、协程的区别是什么?
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
这道题看似是操作系统的基础概念题,但现在 Go、Kotlin 协程这么火,面试官问这个的频率越来越高了。很多 Java 候选人对进程和线程还能说说,一提到协程就露馅。能把这三个概念的关系和本质讲清楚,基本就能看出你的技术视野有多宽。
面试考察点
-
操作系统基础:面试官不仅仅是想知道你背不背得出来定义,更是想看你能不能从 资源分配 和 调度 两个维度来区分三者。理解了这两个维度,很多概念就串起来了。
-
协程认知:考察你是否了解协程的本质——用户态线程,以及它为什么比线程轻量。这块答好了说明你技术视野不局限于 Java。
-
Java 关联:能不能把进程、线程、协程和 Java 的实际应用联系起来,比如 JVM 进程模型、Java 线程与 OS 线程的映射关系、Java 19 虚拟线程(协程的 Java 实现)。
核心答案
一句话概括三者的关系:进程是资源分配的基本单位,线程是 CPU 调度的基本单位,协程是用户态的轻量级线程。
上图展示了三者的层级包含关系。简单来说:
- 进程 是操作系统分配资源的基本单位,每个进程有独立的内存空间
- 线程 是进程内的执行单元,多个线程共享进程的资源,由操作系统调度
- 协程 是线程内的用户态调度单元,由程序自己控制切换,对操作系统透明
核心对比表格:
| 维度 | 进程 | 线程 | 协程 |
|---|---|---|---|
| 调度方 | 操作系统 | 操作系统 | 用户程序 |
| 切换开销 | 大(涉及内存空间切换) | 中(寄存器、栈切换) | 极小(用户态栈切换) |
| 内存占用 | 独立地址空间(MB 级) | 共享进程内存,独立栈(MB 级) | 几 KB |
| 创建成本 | 高(fork) | 中(系统调用) | 极低(用户态对象) |
| 并发数量 | 几十~几百 | 几百~几千 | 几万~几百万 |
| 通信方式 | 管道、消息队列、共享内存 | 共享内存(需同步) | 共享内存(无需同步) |
| 是否安全 | 一个进程崩溃不影响其他 | 一个线程崩溃可能拖垮整个进程 | 一个协程挂起不影响其他 |
深度解析
一、进程(Process)
进程是操作系统资源分配的最小单位。每个进程都有自己独立的内存空间(代码段、数据段、堆、栈),进程之间互不干扰。
// Java 中每个运行的 JVM 就是一个进程
public static void main(String[] args) {
// 获取当前进程信息
ProcessHandle current = ProcessHandle.current();
System.out.println("PID: " + current.pid());
System.out.println("进程信息: " + current.info());
}
进程的核心特点:
- 隔离性强:每个进程的内存空间独立,一个进程挂了不会影响其他进程。这也是 Chrome 每个标签页用独立进程的原因——一个页面崩了不影响其他页面。
- 创建开销大:创建进程需要分配独立的内存空间、复制父进程的资源(写时复制),代价不低。
- 通信复杂:进程间通信(IPC)需要管道、消息队列、共享内存等机制,比线程间通信麻烦得多。
二、线程(Thread)
线程是 CPU 调度的最小单位,也是操作系统层面并发执行的基本单位。同一个进程内的多个线程共享进程的内存空间和资源,但每个线程有自己独立的栈和程序计数器。
Java 线程和操作系统线程是 1:1 的关系(传统模型),每个 Java 线程都对应一个 OS 线程。
上图展示了线程切换的过程。核心问题在于:线程的调度由操作系统内核完成,每次切换都要从用户态切换到内核态,保存和恢复寄存器、程序计数器等上下文,还可能导致 CPU 缓存(TLB)失效。一次线程切换的开销大约在 1~10 微秒。
听起来不多?但如果你有上万个线程频繁切换,这个开销就非常可观了。这也是为什么线程数不能无限创建——光上下文切换就能把 CPU 吃满。
三、协程(Coroutine)
协程,也叫 "用户态线程" 或 "轻量级线程"。和线程最大的区别是:协程的切换由程序自己控制,不需要操作系统内核参与。
上图展示了协程切换的过程。因为协程完全在用户态完成调度,不需要陷入内核态,所以切换速度极快。而且协程不需要操作系统分配栈空间(线程栈通常是 1MB),一个协程可能只需要 几 KB 的内存。
这意味着什么?一台机器上跑几百个线程就觉得吃力了,但跑 几十万个协程 完全没问题。Go 语言之所以在高并发场景下这么能打,很大程度就是因为 goroutine(Go 的协程)足够轻量。
四、三者的切换开销对比
这是面试中最核心的对比,直接看数据更直观:
| 维度 | 进程切换 | 线程切换 | 协程切换 |
|---|---|---|---|
| 切换模式 | 用户态 ↔ 内核态 | 用户态 ↔ 内核态 | 纯用户态 |
| 需要切换地址空间 | ✅ 是 | ❌ 否(同进程) | ❌ 否 |
| 需要 OS 参与 | ✅ 是 | ✅ 是 | ❌ 否 |
| 寄存器保存/恢复 | 全量 | 部分 | 仅用户态寄存器 |
| TLB 缓存影响 | 失效(严重) | 可能失效 | 不影响 |
| 典型耗时 | 10~100 微秒 | 1~10 微秒 | 0.1~1 微秒 |
切换开销:进程 >> 线程 >> 协程,差了差不多 1~2 个数量级。
五、Java 中的协程——虚拟线程(Virtual Threads)
Java 19 引入了虚拟线程(JEP 444,正式版在 Java 21),这就是 Java 版的协程。
// Java 21+ 虚拟线程
Thread.startVirtualThread(() -> {
System.out.println("我是虚拟线程,非常轻量!");
});
// 或者使用虚拟线程工厂创建线程池
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// 轻松创建上万个 "线程"
for (int i = 0; i < 100_000; i++) {
executor.submit(() -> {
// IO 操作时,虚拟线程自动让出载体线程
// 不阻塞 OS 线程
return httpClient.send(request);
});
}
}
虚拟线程的核心设计:
- M:N 调度模型:M 个虚拟线程映射到 N 个平台线程(OS 线程)上。JVM 内部自己管理调度,不再 1:1 对应 OS 线程。
- 自动让出:当虚拟线程遇到 IO 阻塞(网络请求、文件读写等)时,自动让出底层的平台线程,给其他虚拟线程用。等 IO 完成后再恢复执行。
- API 兼容:虚拟线程还是
java.lang.Thread,现有的synchronized、ReentrantLock、线程池等 API 都能直接用,迁移成本很低。
这块面试的时候提一嘴虚拟线程,面试官绝对加分。说明你不仅在跟 Java 的发展,还理解协程的本质和 Java 的实现方式。
六、常见误区
误区 1:协程比线程快,所以协程能替代线程。
不完全对。协程在 IO 密集型 场景下确实优势巨大(大量 IO 等待,线程切换浪费严重)。但在 CPU 密集型 计算场景下,协程和线程的性能差距不大,甚至线程可能更快(因为线程可以真正并行利用多核,而同一时刻一个线程上只有一个协程在运行)。
误区 2:Java 没有协程。
Java 21 的虚拟线程就是协程。虽然实现方式(M:N 模型 + 自动 yield)和 Go 的 goroutine 不完全一样,但核心思想一致——用户态调度、轻量级、高并发。
误区 3:多线程就一定能利用多核。
多线程只是提供了利用多核的 可能性。但如果线程都在等锁、等 IO,那 CPU 照样闲着。协程的思路不一样——用少量线程 + 大量协程,在 IO 等待时快速切换到其他协程,把 CPU 吃满。
面试高频追问
-
Java 线程和 OS 线程是什么关系?
- 传统 Java 线程是 1:1 模型,每个 Java 线程对应一个操作系统线程。Java 21 的虚拟线程改为 M:N 模型,多个虚拟线程映射到少量 OS 线程上。
-
协程为什么轻量?
- 三个原因:用户态调度不需要内核参与、栈空间只需要几 KB(线程通常 1MB)、创建和切换不需要系统调用。
-
Go 的 goroutine 和 Java 虚拟线程有什么区别?
- 核心思想一致,但实现细节不同。goroutine 使用自己的调度器(GMP 模型),栈初始只有 2KB 且可动态伸缩;虚拟线程基于
ForkJoinPool调度,API 上完全兼容Thread。
- 核心思想一致,但实现细节不同。goroutine 使用自己的调度器(GMP 模型),栈初始只有 2KB 且可动态伸缩;虚拟线程基于
常见面试变体
- "为什么有了线程还需要协程?"
- "Java 21 的虚拟线程是什么?解决了什么问题?"
- "Go 的 goroutine 为什么比 Java 线程轻量?"
记忆口诀
进程分配资源,线程调度执行,协程用户态切换。创建成本和切换开销:进程 > 线程 > 协程。协程的优势在 IO 密集,不在 CPU 密集。
总结
面试答这道题,三层递进:先用一句 "进程是资源分配单位、线程是调度单位、协程是用户态线程" 给出核心结论,再从切换开销角度讲清楚三者的性能差异(关键点:内核态 vs 用户态),最后点一下 Java 21 虚拟线程作为加分项。能把内核态切换和用户态切换的区别讲明白,这道题就稳了。