面试知识库
进阶

协程原理#

一句话答案#

协程是用户态轻量级线程,切换不需内核介入成本极低,可创建百万级。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 密集不适合
  • ❌ 虚拟线程就是绿色线程——实现不同