线程、进程、协程有什么区别?

#Java#面试八股

线程、进程、协程有什么区别?

结论先行:进程是操作系统分配资源的最小单位,线程是CPU 调度的最小单位,协程则是由运行时在用户态自己调度的执行单元。三者的分水岭只有两句话:谁持有资源、谁决定什么时候切换。

1. 一张表划清边界

维度进程线程协程
资源归属独立地址空间、独立文件句柄表共享进程的堆与方法区,独享栈和 PC共享所在线程的全部资源,只保留自己的栈帧
切换由谁做内核内核用户态运行时(Go runtime、JVM 虚拟线程调度器)
切换代价页表切换、TLB 失效,微秒到毫秒保存寄存器、切内核栈,约 1~10 微秒保存少量寄存器,纳秒级
隔离性强,进程崩溃互不影响弱,一个线程 OOM 拖垮整个 JVM弱于线程,与所在线程同生共死
通信方式管道、共享内存、Socket,都要过内核共享变量 + 同步原语,几乎零成本共享变量或 channel
能否真并行能能(多核)单线程内只能并发,不能并行

2. 内存布局决定了一切

进程(独立地址空间、独立页表)
├── 代码段 / 数据段 / 堆        ← 所有线程共享
├── 线程 1(内核调度实体)
│   ├── 虚拟机栈 + 本地方法栈    ← 线程私有
│   ├── 程序计数器
│   └── 协程 A / 协程 B          ← 跑在线程 1 的栈上,轮流执行
└── 线程 2(内核调度实体)
    └── 虚拟机栈 + 程序计数器

这张图解释了两个常见现象:

  1. 栈私有:所以 StackOverflowError 只打死抛出的那个线程,不影响别的线程;
  2. 堆共享:所以堆内存耗尽时 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)——等外部就用协程把等待变成廉价挂起,算就用多线程把核吃满。

笔记加载中…