协程原理#
一句话答案#
协程是用户态轻量级线程,切换不需内核介入成本极低,可创建百万级。Java 21 虚拟线程(Virtual Thread)是 JDK 提供的轻量级用户态线程机制,可类比协程。
核心要点
vs 线程: 协程用户态调度(无内核切换开销) / 更轻量(KB级栈) / 可创建百万级
Java 虚拟线程(JDK 21): Thread.ofVirtual().start(() -> task())
原理: 虚拟线程挂载在载体线程(平台线程)上,IO阻塞时自动卸载让出载体线程
协程切换为什么只需几百纳秒#
把协程切换和线程切换逐项对比,就能看出快在”省掉了几乎所有内核操作”:
| 操作 | 线程切换(内核态) | 协程切换(用户态) |
|---|---|---|
| 进内核 / 系统调用 | 要(陷入内核、用户↔内核态切换) | 不要,纯用户态函数调用 |
| 切页表(换 CR3) | 跨进程切换时要 | 不要,同进程同地址空间 |
| 刷 TLB | 切页表会导致 TLB 失效 | 不要,TLB 全部保留 |
| 走调度器 | 要(OS 调度器选下一个线程,可能涉及锁、负载均衡) | 不要,协程自己 yield,由用户态调度器直接指定下一个 |
| 保存 / 恢复现场 | 完整寄存器组 + 内核栈 | 只存几个寄存器 + 换栈指针 SP |
本质: 协程切换就是在用户态保存当前几个寄存器(PC、SP、几个被调用者保存寄存器),把栈指针指向另一个协程的栈,然后跳过去——一段几十条指令的纯用户态代码。它不陷入内核、不切地址空间、不刷 TLB(这是线程切换最贵的隐性成本,TLB 冷启动后续访存全慢)、也不经过 OS 调度器。所以是几百纳秒量级,而线程切换是微秒量级(差一到两个数量级)。
有栈协程 vs 无栈协程:
| 类型 | 代表 | 实现 | 特点 |
|---|---|---|---|
| 有栈协程 | Go goroutine、Java 虚拟线程 | 每个协程有独立栈,切换时换 SP;可在任意嵌套函数深处挂起 | 灵活(任意位置 yield),但每协程占一段栈内存 |
| 无栈协程 | C++20 协程、Rust async、JS async/await | 编译器把函数改写成状态机,挂起点的局部变量存进堆上的状态对象,不需要独立运行栈 | 更省内存、切换更轻;但只能在标了 await 的点挂起,有”函数染色”问题 |
一句话:有栈协程靠”换真实栈”实现挂起,无栈协程靠”编译器把函数拆成状态机”实现挂起。
面试回答(2分钟版)
协程是用户态的轻量级线程,最大的特点是切换完全在用户态完成,不需要内核介入,所以切换成本极低。传统线程是OS调度的,每个线程需要MB级别的栈空间,上下文切换涉及内核态切换开销很大;而协程只需要KB级别的栈空间,可以轻松创建数十万甚至百万级别的并发。Java 21引入的虚拟线程(Virtual Thread)是JDK提供的轻量级用户态线程机制,可类比其他语言中的协程或green threads,使用方式很简单,Thread.ofVirtual().start()就可以创建。虚拟线程的原理是挂载在载体线程(平台线程)上执行,当遇到IO阻塞操作时会自动从载体线程上卸载,让出载体线程给其他虚拟线程使用,IO完成后再调度回来继续执行。这特别适合IO密集型场景,比如大量的HTTP调用或数据库查询。但要注意虚拟线程不适合CPU密集型任务,因为CPU密集意味着不会主动让出载体线程,和普通线程没有区别。另外虚拟线程中要避免使用synchronized,因为它会pin住载体线程,应该改用ReentrantLock。
追问与易错
追问方向:
- “Java 虚拟线程和 Go goroutine 区别?”→ 都是用户态轻量级线程;goroutine 有独立调度器(GMP 模型)和栈增长机制,虚拟线程挂载在平台线程上遇 IO 自动卸载,两者设计哲学不同但目标一致
- “虚拟线程的限制?”→ 不适合 CPU 密集型任务(不会主动让出载体线程)、避免使用 synchronized(会 pin 住载体线程应改用 ReentrantLock)、ThreadLocal 开销可能变大
- “什么场景适合虚拟线程?”→ IO 密集型场景:大量 HTTP 调用、数据库查询、文件读写等,虚拟线程在 IO 等待时自动让出载体线程,用少量平台线程支撑海量并发
易错点:
- ❌ 虚拟线程解决所有并发问题——CPU 密集不适合
- ❌ 虚拟线程就是绿色线程——实现不同