⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 http://www.iocoder.cn/Fight/pinduoduoermianweishenmeyoulexianchenghaixuyaoxiechengwoexiechengshisha/ 「芋道源码」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

面试官问”进程、线程、协程的区别”,大多数人背完定义就停了。但拼多多二面问的是”为什么有了线程还需要协程”——这是在考你能不能从调度成本的角度把三者串成一条逻辑链。

这道题考的不是定义,是思维层次

面试官真正想听的是三件事:

  1. 资源分配 vs 调度:你能不能用这两个维度一刀切开进程和线程?
  2. 内核态 vs 用户态:你知不知道线程切换贵在哪?协程便宜在哪?
  3. Java 跟进:你有没有关注 Java 21 虚拟线程——这是 Java 对协程的正式回应。

三层递进答完,这道题稳拿。

一句话说清三者关系

进程是资源分配的基本单位,线程是 CPU 调度的基本单位,协程是用户态的轻量级线程。

核心对比一张表说清楚:

维度 进程 线程 协程
调度方 操作系统 操作系统 用户程序
切换开销 大(内存空间切换) 中(寄存器、栈切换) 极小(用户态栈切换)
内存占用 独立地址空间(MB 级) 共享进程内存,独立栈(MB 级) 几 KB
创建成本 高(fork) 中(系统调用) 极低(用户态对象)
并发上限 几十~几百 几百~几千 几万~几百万
通信方式 管道、消息队列、共享内存 共享内存(需同步) 共享内存(无需锁)
崩溃影响 一个进程崩不影响其他 一个线程崩可能拖垮整个进程 一个协程挂起不影响其他

看最右列,协程在每个维度上都碾压。但别急着下结论——后面讲适用场景。

进程:隔离性拉满,但太重了

进程是操作系统资源分配的最小单位。每个进程有独立的内存空间(代码段、数据段、堆、栈),进程之间互不干扰。

// Java 中每个运行的 JVM 就是一个进程
ProcessHandle current = ProcessHandle.current();
System.out.println("PID: " + current.pid());

进程的核心特点:

  • 隔离性强:一个进程挂了不影响其他。Chrome 每个标签页用独立进程就是这个道理——一个页面崩了其他照常。
  • 创建开销大:要分配独立内存空间、复制父进程资源(写时复制),代价不低。
  • 通信复杂:进程间通信(IPC)要走管道、消息队列、共享内存,比线程间通信麻烦一个量级。

一句话总结:进程隔离好但太重,不适合高频创建销毁的并发场景。

线程:能用,但扛不住十万并发

线程是 CPU 调度的最小单位。同一进程内的多个线程共享内存空间和资源,但每个线程有自己独立的栈和程序计数器。

Java 线程和 OS 线程是 1:1 的关系——每个 new Thread() 背后都对应一个操作系统线程。

核心问题在于切换成本: 线程调度由内核完成,每次切换要从用户态陷入内核态,保存恢复寄存器、程序计数器,还可能导致 TLB 缓存失效。一次切换大约 1~10 微秒。

听起来不多?但 1 万个线程频繁切换,光上下文切换就能把 CPU 吃满。这也是为什么线程数不能无限加——线程越多,花在切换上的时间比例越大,真正干活的时间反而更少。

而且每个线程默认栈空间 1MB,1 万个线程就是 10GB 内存。光栈空间就把机器撑爆了。

协程:用户态调度才是正解

协程的本质:把调度权从内核收回到用户程序手里。

线程切换贵在哪?贵在要陷入内核态。协程完全在用户态完成调度——不需要系统调用,不需要内核参与,不需要保存完整寄存器上下文。切换速度快到 0.1~1 微秒。

而且协程不需要操作系统分配栈空间。一个协程可能只需要 几 KB 内存,一台机器跑几十万个协程完全没问题。

Go 语言之所以在高并发场景下能打,核心就是 goroutine——初始栈只有 2KB,可动态伸缩,百万级并发轻轻松松。

切换开销到底差多少?两个数量级

这是面试最核心的对比:

维度 进程切换 线程切换 协程切换
切换模式 用户态 ↔ 内核态 用户态 ↔ 内核态 纯用户态
需要切换地址空间 是 否(同进程) 否
需要 OS 参与 是 是 否
寄存器保存/恢复 全量 部分 仅用户态寄存器
TLB 缓存影响 失效(严重) 可能失效 不影响
典型耗时 10~100 微秒 1~10 微秒 0.1~1 微秒

进程 >> 线程 >> 协程,每级差 1~2 个数量级。

为什么差这么多?因为进程和线程切换要陷入内核态——这一步涉及特权级切换、寄存器全量保存、可能的 TLB 刷新。协程完全绕过了这些,只在用户态保存几个寄存器就完事。

Java 21 虚拟线程:迟到十年的补课

Go 有 goroutine,Kotlin 有 coroutine,Java 等到 21 版本才正式推出虚拟线程(JEP 444)。

// Java 21 虚拟线程——创建 10 万个"线程"毫无压力
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 100_000; i++) {
executor.submit(() -> {
// 遇到 IO 阻塞时自动让出载体线程
return httpClient.send(request, BodyHandlers.ofString());
});
}
}

虚拟线程的核心设计:

  • M:N 调度模型:M 个虚拟线程映射到 N 个平台线程上。JVM 自己管调度,不再 1:1 对应 OS 线程。
  • 自动让出:虚拟线程遇到 IO 阻塞时,自动让出底层平台线程给其他虚拟线程用。IO 完成后再恢复。
  • API 完全兼容:虚拟线程还是 java.lang.Thread,现有的 synchronized、ReentrantLock、线程池 API 直接用,迁移成本极低。

面试时提一嘴 Java 21 虚拟线程,说明你在跟 Java 的演进方向,绝对加分。

三个常见误区,踩一个扣一分

误区 1:协程比线程快,所以协程能替代线程。

错。协程在 IO 密集型 场景优势巨大——大量 IO 等待时线程切换白白浪费 CPU。但在 CPU 密集型 场景,协程和线程性能差距不大,甚至线程可能更快——因为线程能真正并行利用多核,而同一时刻一个线程上只有一个协程在跑。

误区 2:Java 没有协程。

Java 21 虚拟线程就是协程。实现方式和 Go 的 goroutine 不完全一样(Java 用 ForkJoinPool 调度,Go 用 GMP 模型),但核心思想一致——用户态调度、轻量级、高并发。

误区 3:多线程就能利用多核。

多线程只是提供了利用多核的可能性。线程都在等锁、等 IO,CPU 照样闲着。协程的思路不同——用少量线程 + 大量协程,IO 等待时快速切换到其他协程,把 CPU 利用率拉满。

面试追问怎么接

Q:Java 线程和 OS 线程什么关系?

传统 Java 线程是 1:1 模型,每个 Java 线程对应一个 OS 线程。Java 21 虚拟线程改为 M:N 模型。

Q:协程为什么轻量?

三点:用户态调度不需内核参与、栈空间只要几 KB(线程 1MB)、创建和切换不走系统调用。

Q:goroutine 和 Java 虚拟线程有什么区别?

核心思想一致,实现细节不同。goroutine 用 GMP 调度模型,初始栈 2KB 可动态伸缩;虚拟线程基于 ForkJoinPool 调度,API 兼容 Thread 类。


最后记住一句话: 进程分配资源,线程调度执行,协程用户态切换。创建成本和切换开销:进程 > 线程 > 协程,每级差两个数量级。协程的优势在 IO 密集——这就是”为什么有了线程还需要协程”的终极答案。

文章目录
  1. 1. 这道题考的不是定义,是思维层次
  2. 2. 一句话说清三者关系
  3. 3. 进程:隔离性拉满,但太重了
  4. 4. 线程:能用,但扛不住十万并发
  5. 5. 协程:用户态调度才是正解
  6. 6. 切换开销到底差多少?两个数量级
  7. 7. Java 21 虚拟线程:迟到十年的补课
  8. 8. 三个常见误区,踩一个扣一分
  9. 9. 面试追问怎么接