进程、线程、协程与Java虚拟线程

发布时间:2026/6/24 2:07:13
进程、线程、协程与Java虚拟线程 进程、线程、协程与Java虚拟线程Java 开发者每天都在用线程池、CompletableFuture、Async但底层到底是怎么回事为什么 Java 21 的虚拟线程被称为「革命性」特性本文从 OS 层面一路讲回 JVM用三层递进的方式串起进程、线程、协程最后深入 Java 虚拟线程。文章目录进程、线程、协程与Java虚拟线程一、进程操作系统眼中的「程序」什么是进程进程的内存布局进程的关键特性二、线程CPU 眼中的「执行流」什么是线程线程 vs 进程内核线程 vs 用户线程三、协程程序员眼中的「暂停与恢复」什么是协程线程 vs 协程一张图看懂有栈协程 vs 无栈协程四、Java 虚拟线程Project Loom 的革命传统 Java 线程的「阿喀琉斯之踵」虚拟线程怎么解决的代码对比传统方式 vs 虚拟线程虚拟线程什么场景下「无敌」什么时候不该用虚拟线程性能数据虚拟线程 vs Go goroutine五、一张脑图收尾一句话总结一、进程操作系统眼中的「程序」什么是进程进程是 OS 资源分配的基本单位。双击一个程序 → OS 创建一个进程。进程 独立的内存空间代码段 数据段 堆 栈 系统资源文件句柄、网络 Socket、信号处理... 至少一个线程主线程进程的内存布局高地址 ┌─────────────┐ │ 栈 (Stack) │ ← 函数调用、局部变量、向下增长 │ ↓ │ │ ↑ │ │ 堆 (Heap) │ ← new 出来的对象、malloc向上增长 ├─────────────┤ │ 数据段 (BSS) │ ← 全局/静态变量 ├─────────────┤ │ 代码段 (Text)│ ← 编译后的机器指令只读 低地址 └─────────────┘进程的关键特性特性说明影响内存隔离每个进程独立的虚拟地址空间A 进程崩了不连累 B但通信成本高切换代价大切换页表 刷新 TLB 缓存可能失效微秒级通信 (IPC)管道、消息队列、共享内存、Socket、信号全都很「重」数量有限每个进程 GB 级内存一台机器跑几十到几百个比喻进程 独立别墅。每人一栋自带水电网。邻居着火与你无关但想串门得先敲大门IPC。二、线程CPU 眼中的「执行流」什么是线程线程是 CPU 调度的基本单位。一个进程可以包含多个线程它们共享进程的内存空间。进程 ⊃ 线程1, 线程2, 线程3, ... 共享堆、全局变量、文件句柄、代码段 独有栈、寄存器上下文、程序计数器 (PC)线程 vs 进程┌─────────────────────────────────────────┐ │ 进程 (独立别墅) │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ 线程 1 │ │ 线程 2 │ │ 线程 3 │ │ │ │ 私有栈 │ │ 私有栈 │ │ 私有栈 │ │ │ │ PC寄存器│ │ PC寄存器│ │ PC寄存器│ │ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ └───────────┼───────────┘ │ │ ▼ │ │ 共享堆 全局变量 文件 │ └─────────────────────────────────────────┘维度进程线程调度者OSOS内存独立地址空间 (GB)共享堆私有栈 (~1MB)切换代价大页表 TLB 缓存中寄存器 栈切换通信IPC管道/Socket/共享内存共享变量快但需要同步隔离性强一个崩不影响其他弱一个线程崩可能带崩进程创建销毁慢fork 资源分配较快数量上限几十~几百几千受限于栈空间比喻线程 别墅里的室友。共用厨房堆内存各睡各的卧室私有栈。好处是沟通快共享变量坏处是一个室友在厨房纵火内存越界大家都遭殃。内核线程 vs 用户线程内核线程 (1:1) 用户线程 (N:1) 混合模型 (N:M) ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ T1 │ │ T2 │ │ T1 │ │ T2 │ │ T3 │ │ T1 │ │ T2 │ │ T3 │ └─┬──┘ └─┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ │ │ └──────┼──────┘ │ │ │ ┌─▼────────▼─┐ │ ┌──────▼──────▼──────▼──────┐ │ 内核线程1 │ ┌───▼───┐ │ 用户态调度器 │ │ 内核线程2 │ │内核线程│ │ ┌─────┐ ┌─────┐ │ └────────────┘ └───────┘ │ │KLT 1│ │KLT 2│ │ └──┴─────┴──┴─────┴───────┘ Java Thread 早期 Green Thread Go goroutine (1:1阻塞 真阻塞) (一个阻塞 全体阻塞) Java Virtual Thread (阻塞时自动切换)三、协程程序员眼中的「暂停与恢复」什么是协程协程是用户态的轻量级执行单元——和线程最关键的区别协程切换不经过 OS 内核。线程切换用户态 → 系统调用 → 内核态 → 保存上下文 → 调度 → 恢复 → 用户态 ↑ 微秒级涉及特权级切换 协程切换用户态 → 保存少量寄存器 栈指针 → 跳到另一个协程 ↑ 纳秒级纯函数调用级别的开销线程 vs 协程一张图看懂线程模型抢占式 协程模型协作式 线程A 线程B 线程C 协程A 协程B 协程C │ │ │ │ │ │ │ ✂───┤ │ ← OS 时钟中断 │ ✂───┤ │ ← 自己 yield │ │ │ 强行切换 │ │ │ 主动让出 ├──────┤ ✂───┤ ├──────┤ │ │ │ │ │ ├──────┤ ✂──────┼──────┤ │ │ ✂── 自己 yield │ │ │ │ │ │维度线程协程调度器OS 内核用户态运行时Go scheduler / JVM / asyncio切换代价微秒级内核态纳秒级纯用户态调度策略抢占式时间片到期强行切换协作式自己 yield不抢内存占用~1MB栈固定分配几 KB栈动态增长创建数量几千几十万~百万阻塞影响线程阻塞 内核线程也阻塞协程「阻塞」 挂起自己调度器执行其他协程并发安全需要锁/Mutex/volatile协作式调度天然无竞态同一时刻一个线程只跑一个协程比喻线程 公司雇了 3 个专职员工老板OS决定谁干活谁休息。协程 你一个人同时做 3 件事烧水协程 A→ 水没开切菜协程 B→ 等下锅洗碗协程 C→ 切换成本不是「换个人」而是「换个姿势」。有栈协程 vs 无栈协程有栈协程 (Stackful)无栈协程 (Stackless)代表Go goroutineKotlin suspend、C20 协程、JS async/await实现每个协程有独立栈可在任意深度切换编译期将函数拆成状态机只能在 suspend 点切换内存每个协程 2~8KB 栈几乎无额外内存状态机大小灵活性高——任意位置可暂停低——只能标记了 suspend 处暂停染色问题无透明有——async 函数会「传染」调用者// 无栈协程的「染色问题」——一个函数标了 suspend调用者也得标suspendfunfetchUser():User{...}suspendfunprocessUser(){fetchUser()}// 传染四、Java 虚拟线程Project Loom 的革命传统 Java 线程的「阿喀琉斯之踵」Java 从诞生起Thread就是 1:1 映射到 OS 内核线程的。在 Web 服务场景下这成了瓶颈// 一个典型的 Web 请求处理GetMapping(/order/{id})publicOrdergetOrder(PathVariableLongid){// 线程在这里阻塞 99% 的时间OrderorderorderDao.findById(id);// 等数据库 50msUseruseruserService.getUser(uid);// 等 RPC 100msInventoryinvinventoryService.check(id);// 等 RPC 80msreturnassemble(order,user,inv);}// 线程在等但 OS 内核线程也跟着一起等 → 浪费问题链条1 个请求占用 1 个线程 → 线程在等 I/OCPU 空闲 → 但线程数有上限每个线程 ~1MB 栈8GB 内存约 4000 个 → 高并发时线程池耗尽 → 请求排队/超时 → 解决方案加机器花钱或异步编程地狱虚拟线程怎么解决的虚拟线程 JVM 管理的用户态线程N:M 映射到少量平台线程OS 内核线程。┌──────────────────────┐ 虚拟线程 × 100 万 │ VT₁ VT₂ VT₃ ... │ ← JVM 调度器管理 │ 每个只占几百字节 │ (ForkJoinPool) └──────────┬───────────┘ │ N:M 动态映射 ┌──────────▼───────────┐ 平台线程 × N │ OS Thread₁ ... │ ← N ≈ CPU 核心数 (传统内核线程) │ OS Threadₙ │ └──────────────────────┘核心魔法当虚拟线程遇到阻塞操作时间线 → 平台线程 1 ████████ VT_A ██░░░░░░░░░░░░░░░░░░████ VT_C ████████ 平台线程 2 ████████ VT_B ████████████████████████████████████████ VT_A 执行中 → db.query()阻塞 → JVM 检测到阻塞 → 把 VT_A 的栈帧卸下来unmount → 平台线程 1 立即接手 VT_C 继续执行 → ...数据库返回... → VT_A 的栈帧装回去mount等待任意空闲平台线程继续 → 平台线程永不空闲关键洞察阻塞操作发生时JVM 在底层做了yield开发者感知不到。你写的是同步代码跑出来的效果却是异步的。代码对比传统方式 vs 虚拟线程// 方式一传统线程池池耗尽了请求就排队ExecutorServicepoolExecutors.newFixedThreadPool(200);pool.submit(()-{Stringadb.call();// 阻塞线程被占用Stringbapi.call();// 阻塞线程继续被占用returnab;});// 方式二CompletableFuture 异步代码可读性灾难CompletableFutureStringfutureCompletableFuture.supplyAsync(()-db.call()).thenCompose(a-CompletableFuture.supplyAsync(()-api.call()).thenApply(b-ab));// 方式三虚拟线程同步写法 异步效果清爽Thread.startVirtualThread(()-{Stringadb.call();// 底层自动 yield不占用平台线程Stringbapi.call();// 同上returnab;});// 或配合 ExecutorServicetry(varexecutorExecutors.newVirtualThreadPerTaskExecutor()){executor.submit(()-doWork());}不需要async/await关键字不需要改代码风格——只需把newFixedThreadPool换成newVirtualThreadPerTaskExecutor。虚拟线程什么场景下「无敌」虚拟线程最适合的区域 CPU 密集型 ←───├───→ I/O 密集型 不重要 │ 这里是主场 示例 示例 - 视频编码 - HTTP API 调用等网络 - 科学计算 - 数据库查询等磁盘 - 加密解密 - 消息队列消费等消息 - 微服务编排等下游黄金场景Web 服务器处理请求、微服务调用链、数据库访问——凡是「大部分时间在等」的场景。什么时候不该用虚拟线程不适合的场景原因纯 CPU 计算虚拟线程不会加速计算反而有调度开销synchronized 块内有 I/Osynchronized会pin住平台线程虚拟线程无法 unmount。JDK 21 已有 partial fixJDK 24 彻底解决native 代码中有阻塞JVM 感知不到 native 层的阻塞需要线程优先级/守护线程精细控制虚拟线程不支持setPriority()性能数据Spring Boot 3.2 虚拟线程Tomcat vs Spring Boot 3.2 传统线程池Tomcat 并发连接数 5000每个请求内 sleep 100ms 模拟 I/O 传统线程池200 线程吞吐量 ~2,000 req/sP99 延迟 ~2.5s 虚拟线程 吞吐量 ~50,000 req/sP99 延迟 ~120ms ↑ 25 倍吞吐量提升延迟降低 95% 来源Spring 官方 Blog2023虚拟线程 vs Go goroutineJava 虚拟线程Go goroutine出现版本JDK 212023Go 1.02012映射模型N:M虚拟线程 → 平台线程N:Mgoroutine → OS 线程调度器ForkJoinPoolwork-stealingGo Schedulerwork-stealing栈堆上分配动态增长堆上分配动态增长初始 2KB阻塞处理自动 unmount自动切换抢占JDK 21 开始支持Thread.yield()提示Go 1.14 开始支持异步抢占编程体验同步代码无需awaitgo func()无需await成熟度新特性生态适配中十余年打磨极致成熟五、一张脑图收尾并发编程 │ ┌──────────────┼──────────────┐ ▼ ▼ ▼ 进 程 线 程 协 程 (资源分配单位) (CPU调度单位) (执行流组织) │ │ │ 独立内存空间 共享进程内存 共享线程内存 GB 级占用 MB 级占用 KB 级占用 内核态隔离 内核态切换 用户态切换 ✨ │ │ │ │ └──────┬───────┘ │ │ ▼ ▼ 多进程架构 Java 虚拟线程 (JDK 21) (Nginx) N:M 映射到平台线程 阻塞 yield自动挂起 百万并发不是梦一句话总结进程OS 分配资源的「独立别墅」——隔离强、切换重线程进程里的「室友」——共享内存、比进程轻、但仍有内核切换开销协程线程里的「多面手」——用户态切换、几 KB 栈、创建百万个毫无压力虚拟线程Java 版的 goroutine——同步代码、异步性能、I/O 密集型场景下传统线程池的终结者选择策略CPU 密集用传统线程池I/O 密集用虚拟线程两者可以混用并发编程的本质不是「跑得更快」而是「等得更聪明」。虚拟线程把这个哲学贯彻到了极致。