进程、线程和协程的区别是什么?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(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+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 基础概念是否扎实:光背出三者的定义不算过关,面试官要看你能不能用 “资源分配” 和 “CPU 调度” 这两个关键词,把三者串成一条线。

  2. 对切换成本的理解:上下文切换是并发绕不开的主题,切换开销的差异直接决定了三者的适用场景,这是深挖的重灾区。

  3. 技术视野:是否了解 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 生态。

Java 虚拟线程对比
Java 虚拟线程对比

常见误区

  • “协程能利用多核”——错。单线程内的协程串行执行,多核还得靠多线程
  • “有了协程就不需要线程池”——不完全对。虚拟线程适合高并发 IO 密集场景,CPU 密集型任务还是平台线程 + 线程池更合适
  • “进程切换贵只是因为要保存的东西多”——漏了重点。TLB 失效带来的间接开销往往比直接切换成本更高

面试高频追问

  1. 协程这么轻量,为什么不能完全替代线程?

    • 单线程内协程串行,无法利用多核;CPU 密集型任务协程没有优势;阻塞式调用会拖垮整个线程。
  2. 一个线程崩了,为什么整个进程会挂?

    • 线程共享进程地址空间,一个线程写出内存越界或抛出未处理异常,破坏的是整个进程的公共资产,操作系统直接回收整个进程。
  3. Java 虚拟线程和 Go 的 goroutine 有什么区别?

    • 虚拟线程由 JVM 调度、依托载体线程池,阻塞时 unmount;goroutine 用 GMP 模型,Go 1.14 后支持基于信号的异步抢占(运行超 10ms 由 sysmon 发 SIGURG 信号强制打断)。生态兼容性 vs 调度彻底性,各有取舍。
  4. 什么场景用多进程,什么场景用多线程?

    • 要强隔离、容错(如 Chrome 标签页、Nginx worker)用多进程;要共享数据、低延迟通信用多线程;海量 IO 并发用协程。

常见面试变体

  • “为什么进程切换比线程切换开销大?”(本文第五节就是标准答案)
  • “有了线程为什么还需要协程?”(C10K + 切换成本)
  • “Java 为什么要搞虚拟线程?”(1:1 映射太贵,线程池治标不治本)
  • “说说 Java 里 Thread 和虚拟线程的关系?”(Thread 是内核线程的 1:1 封装)

记忆口诀

进程管资源,线程管调度,协程用户态自己调

再补一句:进程隔离好,线程共享快,协程轻如毛

总结

一句话收尾:进程是资源分配的单位,线程是 CPU 调度的单位,协程是用户态自调度的轻量执行流——三者切换成本依次递减、并发量级依次递增,说到底就是操作系统和语言运行时在不同时代,对 “并发太贵怎么办” 这个问题交出的三代方案。能讲出演进逻辑 + 切换原理 + Java 虚拟线程的落地,这道题就稳了。