线程、进程、协程有什么区别?
结论先行:进程是操作系统分配资源的最小单位,线程是CPU 调度的最小单位,协程则是由运行时在用户态自己调度的执行单元。三者的分水岭只有两句话:谁持有资源、谁决定什么时候切换。
1. 一张表划清边界
| 维度 | 进程 | 线程 | 协程 |
|---|---|---|---|
| 资源归属 | 独立地址空间、独立文件句柄表 | 共享进程的堆与方法区,独享栈和 PC | 共享所在线程的全部资源,只保留自己的栈帧 |
| 切换由谁做 | 内核 | 内核 | 用户态运行时(Go runtime、JVM 虚拟线程调度器) |
| 切换代价 | 页表切换、TLB 失效,微秒到毫秒 | 保存寄存器、切内核栈,约 1~10 微秒 | 保存少量寄存器,纳秒级 |
| 隔离性 | 强,进程崩溃互不影响 | 弱,一个线程 OOM 拖垮整个 JVM | 弱于线程,与所在线程同生共死 |
| 通信方式 | 管道、共享内存、Socket,都要过内核 | 共享变量 + 同步原语,几乎零成本 | 共享变量或 channel |
| 能否真并行 | 能 | 能(多核) | 单线程内只能并发,不能并行 |
2. 内存布局决定了一切
进程(独立地址空间、独立页表)
├── 代码段 / 数据段 / 堆 ← 所有线程共享
├── 线程 1(内核调度实体)
│ ├── 虚拟机栈 + 本地方法栈 ← 线程私有
│ ├── 程序计数器
│ └── 协程 A / 协程 B ← 跑在线程 1 的栈上,轮流执行
└── 线程 2(内核调度实体)
└── 虚拟机栈 + 程序计数器
这张图解释了两个常见现象:
- 栈私有:所以
StackOverflowError只打死抛出的那个线程,不影响别的线程; - 堆共享:所以堆内存耗尽时
OutOfMemoryError会让整个 JVM 进程退出——这也是它们俩排查思路完全不同的原因。
3. Java 里的协程:虚拟线程
JDK 21 的虚拟线程(Virtual Thread)就是 JVM 自己实现的协程。它的关键设计是把栈帧搬到堆上,阻塞时挂起并交还载体线程(carrier thread):
// 十万个任务,若用平台线程会直接 OOM;虚拟线程只占用少量载体线程
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 100_000; i++) {
executor.submit(() -> {
Thread.sleep(java.time.Duration.ofSeconds(1)); // 阻塞即让出载体线程
return null;
});
}
} // 关闭时等待全部结束
对比一下平台线程的写法就明白差别在哪:
// 平台线程:1 个任务 = 1 个 OS 线程,栈默认 1MB(-Xss 可调)
new Thread(() -> doSomething()).start(); // 万级就开始吃力
虚拟线程不是靠"更快的线程"取胜,而是靠"阻塞时不再占着 OS 线程"取胜。所以它只对 IO 密集型任务有意义;CPU 密集型任务本来就不会阻塞,用虚拟线程没有任何收益,甚至因为调度开销略慢。
4. 常见追问
追问一:为什么线程切换比协程贵? 因为线程切换要陷入内核:保存/恢复整套 CPU 上下文、切换内核栈、可能还要刷新 TLB;而协程切换发生在用户态,运行时自己保存几个寄存器就够了,不经过内核。
追问二:协程能替代线程池吗?
不能完全替代。虚拟线程适合"一个请求一个协程"的 IO 场景,但连接池、限流、CPU 密集任务仍然需要明确的并发上限。虚拟线程的数量不受控,把数据库连接池打挂的事故并不少见——通常要配合 Semaphore 限制同时访问下游的数量。
追问三:进程间通信为什么比线程间慢? 线程共享地址空间,读写同一块内存即可;进程各有独立页表,数据必须经过内核缓冲区拷贝,或者依赖共享内存映射。
追问四:为什么 Java 线程栈默认 1MB?
栈要容纳整个调用链的栈帧,太小孩子递归就溢出,太大则线程数上不去(线程数 × 1MB 会直接吃满物理内存)。这也是"线程不是越多越好"的一个物理约束。
5. 成本账:为什么"一个请求一个线程"撑不住
| 资源 | 平台线程 | 虚拟线程 |
|---|---|---|
| 栈空间 | 默认 1MB 连续内存(-Xss) | 几百字节起步,按需扩容,放在堆上 |
| 创建代价 | 数十微秒,涉及系统调用 | 纳秒到微秒级,只是普通 Java 对象 |
| 数量上限 | 数千,受内存与内核参数限制 | 百万级,主要受堆内存限制 |
| 阻塞时 | 一直占着 OS 线程空转等待 | 挂起,载体线程去跑别的协程 |
十万并发如果是十万个平台线程,光栈就要 100GB 虚拟内存,还没算内核调度结构。所以传统做法只能用"线程池 + 队列"把并发压到可控范围;虚拟线程把这块成本降到可以忽略,才有了"一个请求一个线程"这种更直观的编程模型。
6. 常见追问(补充)
追问五:既然有协程,多线程还有必要吗?
有。协程解决的是"阻塞时别占线程",但它仍然跑在线程上,真正的多核并行还是靠多个载体线程。JVM 的虚拟线程默认就用 ForkJoinPool 的多个 worker 作为载体,两者是配合关系而不是替代关系。
追问六:为什么 Java 直到 21 才有虚拟线程?
因为要把"阻塞式 API 在阻塞点自动让出"做透明,需要改造 Thread.sleep、Socket 读写、LockSupport 等底层入口(JEP 425 的 continuation 机制),还要保证 ThreadLocal、synchronized 等既有语义不被破坏,工程量远大于语法糖。
小结
进程管资源、线程管调度、协程管"在等 IO 时别浪费线程"。判断该用哪个,只看一个问题:任务是在等外部(IO)还是在算(CPU)——等外部就用协程把等待变成廉价挂起,算就用多线程把核吃满。