进程、线程和协程的区别是什么?
一则或许对你有用的小广告
欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书
《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于
Spring AI + Spring Boot 3.x + JDK 21...,查看介绍《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/
新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍
截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观
面试考察点
-
基础概念是否扎实:光背出三者的定义不算过关,面试官要看你能不能用 “资源分配” 和 “CPU 调度” 这两个关键词,把三者串成一条线。
-
对切换成本的理解:上下文切换是并发绕不开的主题,切换开销的差异直接决定了三者的适用场景,这是深挖的重灾区。
-
技术视野:是否了解 Java 虚拟线程(JDK 21)、Go 的 goroutine 这些业界实践。能聊到这一层,说明你关注技术演进,而不是只背八股。
核心答案
先甩结论,用一句话记住三者的定位:
- 进程:操作系统进行资源分配的基本单位
- 线程:操作系统进行 CPU 调度的基本单位
- 协程:用户态调度的轻量级执行单元,内核压根感知不到它的存在
一张表看全貌:
| 对比维度 | 进程 | 线程 | 协程 |
|---|---|---|---|
| 定位 | 资源分配的基本单位 | CPU 调度的基本单位 | 用户态调度的执行单元 |
| 地址空间 | 独立 | 共享所属进程的地址空间 | 共享所属线程的地址空间 |
| 调度方 | 内核 | 内核 | 用户程序(运行时库) |
| 切换发生的位置 | 内核态 | 内核态 | 用户态 |
| 切换开销 | 最大,微秒级 | 较小,微秒级 | 极小,纳秒级 |
| 内存占用 | MB 级 | Java 默认栈 1MB | KB 级 |
| 可撑起的并发量级 | 几十到几百 | 几百到几千 | 几万到几百万 |
| 崩溃影响 | 一个进程挂了,别的进程没事 | 一个线程崩了,整个进程完蛋 | 一个协程阻塞,拖累整个线程 |
| 通信方式 | IPC:管道、消息队列、共享内存 | 直接读写共享变量(需加锁) | 同一线程内天然串行,直接通信 |
| 典型代表 | Chrome 多进程架构 | Java Thread |
Go goroutine、Java 虚拟线程、Kotlin 协程 |
深度解析
一、从演进史理解三者:为什么会有这三个东西
很多同学一上来就死记硬背表格。其实换个角度,从 “为什么发明它” 入手,理解会深一个层次。
上图就是整个并发单元的演进脉络,拆开说:
- 第一层:从单进程到多进程。操作系统把 CPU 时间切成片,轮流分给不同进程,宏观上 “同时” 跑多个程序。但每个进程自带独立地址空间,创建、切换、通信都很重。
- 第二层:从进程到线程。一个程序内部往往有多个子任务(比如 Word 里一边打字一边拼写检查),再开进程太浪费,于是把 “执行流” 从进程里拆出来,就成了线程——它们共享进程的地址空间,切换不用换页表。
- 第三层:从线程到协程。互联网来了,一台服务器要同时扛几万连接(C10K 问题),内核线程又贵又少,撑不住。干脆把调度权收回到用户态,自己管理一堆轻量级执行流,这就是协程。
每一代新东西,都是为了解决上一代的痛点。面试时能讲出这条线,比背十个对比维度都加分。
二、进程:一个 “包租公”,资源全归它管
进程是操作系统分配资源的基本单位。你启动一个 JVM,操作系统就创建一个进程,给它划一块独立的地址空间(代码段、数据段、堆、栈),配上文件描述符、信号处理表这些资源。
它的核心特点是隔离。每个进程活在自己的地址空间里,谁也碰不着谁。一个进程崩了,操作系统直接回收它的资源,其他进程该干嘛干嘛。Chrome 就是典型:一个标签页崩了,其他标签页照常活着,因为它每个标签页都是独立进程。
代价也在这儿:进程之间想交换点数据,必须走 IPC(管道、消息队列、共享内存、socket),写起来麻烦,还有拷贝开销。
三、线程:进程的 “打工仔”
线程是 CPU 调度的基本单位。一个进程至少有一个线程(主线程),也可以开多个。关键在于:线程共享进程的家产,但私藏了一点干活工具。
自己独有的:
- 程序计数器(PC):记录当前执行到哪条指令
- 一组寄存器:干活时的工作台
- 栈:局部变量、方法调用帧都在这儿
- 线程 ID、信号屏蔽字这些杂项
跟兄弟们共享的:
- 代码段、数据段、堆
- 打开的文件描述符
- 信号处理函数
共享带来了两个直接后果:
- 好处:线程间通信就是把变量写到堆上,另一个线程直接读,零成本
- 坏处:多线程同时改一个变量就是竞态条件,必须上锁;而且一个线程抛了未捕获异常把栈打崩,整个进程跟着完蛋
四、协程:内核不知道你存在,这就是它快的秘密
协程本质上是一段可以 “暂停 / 恢复” 的执行流,调度完全由用户态的运行时库说了算,内核从头到尾不知情。
这个设计带来的红利,主要就三条:
- 切换白菜价:线程切换必须陷入内核(系统调用),保存一大堆寄存器和内核栈信息;协程切换就在用户态倒腾几个寄存器,纳秒级完事
- 内存白菜价:线程栈以 MB 计(Java 默认 1MB),协程栈以 KB 计(Go goroutine 初始 2KB)
- 想开多少开多少:单机几十万、上百万个协程毫无压力,线程开个几千个系统就喘了
但协程也有个绕不开的短板:单个线程内的协程是串行执行的。协程再牛,同一时刻一个线程上也只有一个协程在跑,想利用多核,还是得靠多线程(或多进程)打底。所以协程不是来替代线程的,是来配合线程的。
还有一点特别容易踩坑:协程怕阻塞式系统调用。一个协程调了个阻塞的 IO,整个线程都被按住,同线程的几十万协程全部陪葬。所以协程框架都要求配合异步 IO 或者对阻塞 API 做改造。
五、上下文切换:三者差异的根源
面试官问完区别,十有八九会追一句 “为什么进程切换比线程切换贵”。来,对照着看:
图里三条链路,成本一条比一条低:
- 进程切换最重。除了常规的寄存器和栈切换,它还要换页表(重写 CR3 寄存器)。页表一换,TLB(地址变换旁路缓存)里缓存的地址映射大面积失效,之后的内存访问都得重新走页表查询,这个间接开销比切换本身还伤
- 线程切换(同进程内)省掉了换页表和刷 TLB 这两座大山,但还是要陷入内核态
- 协程切换压根不进内核,用户态保存几个寄存器就完事
量级感受一下:进程切换微秒级,线程切换也是微秒级但常数更小,协程切换纳秒级,差着一到两个数量级。
六、Java 视角:从 1:1 映射到虚拟线程
聊完操作系统,回到 Java 才是社招的重头戏。
传统 Java 的 Thread 在 Linux 上和内核线程是 1:1 映射的——每 new 一个 Thread,底下就趴着一个内核线程。线程有多贵,Thread 就有多贵,所以 Java 服务端才被迫搞出线程池这套东西来回循环利用。
JDK 21 正式转正的虚拟线程(Project Loom,JEP 444)改变了这个局面:
// JDK 21:直接启动一个虚拟线程
Thread.startVirtualThread(() -> {
System.out.println("我是虚拟线程,由 JVM 调度");
});
// 更实用的玩法:每任务一线程的线程池
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1_000_000; i++) {
executor.submit(() -> {
return fetchFromRemote(); // 模拟远程调用
});
}
}
第二个例子,一百万个任务。用平台线程,线程栈加起来先把内存撑爆;用虚拟线程,轻轻松松跑完。它的原理:虚拟线程跑在少量载体线程(carrier thread)上,遇到阻塞操作时自动 “卸载”(unmount),把载体线程让给别的虚拟线程——用同步写法拿到异步的吞吐量。
Go 的 goroutine 也是同样的思路(GMP 调度模型),所以两者经常被拿来对比:goroutine 抢占式调度更彻底,虚拟线程则胜在兼容既有 Java 生态。
常见误区
- “协程能利用多核”——错。单线程内的协程串行执行,多核还得靠多线程
- “有了协程就不需要线程池”——不完全对。虚拟线程适合高并发 IO 密集场景,CPU 密集型任务还是平台线程 + 线程池更合适
- “进程切换贵只是因为要保存的东西多”——漏了重点。TLB 失效带来的间接开销往往比直接切换成本更高
面试高频追问
-
协程这么轻量,为什么不能完全替代线程?
- 单线程内协程串行,无法利用多核;CPU 密集型任务协程没有优势;阻塞式调用会拖垮整个线程。
-
一个线程崩了,为什么整个进程会挂?
- 线程共享进程地址空间,一个线程写出内存越界或抛出未处理异常,破坏的是整个进程的公共资产,操作系统直接回收整个进程。
-
Java 虚拟线程和 Go 的 goroutine 有什么区别?
- 虚拟线程由 JVM 调度、依托载体线程池,阻塞时 unmount;goroutine 用 GMP 模型,Go 1.14 后支持基于信号的异步抢占(运行超 10ms 由 sysmon 发 SIGURG 信号强制打断)。生态兼容性 vs 调度彻底性,各有取舍。
-
什么场景用多进程,什么场景用多线程?
- 要强隔离、容错(如 Chrome 标签页、Nginx worker)用多进程;要共享数据、低延迟通信用多线程;海量 IO 并发用协程。
常见面试变体
- “为什么进程切换比线程切换开销大?”(本文第五节就是标准答案)
- “有了线程为什么还需要协程?”(C10K + 切换成本)
- “Java 为什么要搞虚拟线程?”(1:1 映射太贵,线程池治标不治本)
- “说说 Java 里
Thread和虚拟线程的关系?”(Thread是内核线程的 1:1 封装)
记忆口诀
进程管资源,线程管调度,协程用户态自己调。
再补一句:进程隔离好,线程共享快,协程轻如毛。
总结
一句话收尾:进程是资源分配的单位,线程是 CPU 调度的单位,协程是用户态自调度的轻量执行流——三者切换成本依次递减、并发量级依次递增,说到底就是操作系统和语言运行时在不同时代,对 “并发太贵怎么办” 这个问题交出的三代方案。能讲出演进逻辑 + 切换原理 + Java 虚拟线程的落地,这道题就稳了。
