news 2026/6/24 2:06:47

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
进程、线程、协程与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、C++20 协程、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% 的时间!Orderorder=orderDao.findById(id);// 等数据库 50msUseruser=userService.getUser(uid);// 等 RPC 100msInventoryinv=inventoryService.check(id);// 等 RPC 80msreturnassemble(order,user,inv);}// 线程在等,但 OS 内核线程也跟着一起等 → 浪费!

问题链条

1 个请求占用 1 个线程 → 线程在等 I/O(CPU 空闲) → 但线程数有上限(每个线程 ~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 虚拟线程

// 方式一:传统线程池(池耗尽了请求就排队)ExecutorServicepool=Executors.newFixedThreadPool(200);pool.submit(()->{Stringa=db.call();// 阻塞,线程被占用Stringb=api.call();// 阻塞,线程继续被占用returna+b;});// 方式二:CompletableFuture 异步(代码可读性灾难)CompletableFuture<String>future=CompletableFuture.supplyAsync(()->db.call()).thenCompose(a->CompletableFuture.supplyAsync(()->api.call()).thenApply(b->a+b));// 方式三:虚拟线程(同步写法 + 异步效果,清爽)Thread.startVirtualThread(()->{Stringa=db.call();// 底层自动 yield,不占用平台线程Stringb=api.call();// 同上returna+b;});// 或配合 ExecutorServicetry(varexecutor=Executors.newVirtualThreadPerTaskExecutor()){executor.submit(()->doWork());}

不需要async/await关键字,不需要改代码风格——只需把newFixedThreadPool换成newVirtualThreadPerTaskExecutor

虚拟线程什么场景下「无敌」?

虚拟线程最适合的区域 CPU 密集型 ←───├───→ I/O 密集型 (不重要) │ (这里是主场!) 示例: 示例: - 视频编码 - HTTP API 调用(等网络) - 科学计算 - 数据库查询(等磁盘) - 加密解密 - 消息队列消费(等消息) - 微服务编排(等下游)

黄金场景:Web 服务器处理请求、微服务调用链、数据库访问——凡是「大部分时间在等」的场景。

什么时候不该用虚拟线程?

不适合的场景原因
纯 CPU 计算虚拟线程不会加速计算,反而有调度开销
synchronized 块内有 I/Osynchronizedpin住平台线程(虚拟线程无法 unmount)。JDK 21 已有 partial fix,JDK 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/s,P99 延迟 ~2.5s 虚拟线程: 吞吐量 ~50,000 req/s,P99 延迟 ~120ms ↑ 25 倍吞吐量提升,延迟降低 95% 来源:Spring 官方 Blog(2023)

虚拟线程 vs Go goroutine

Java 虚拟线程Go goroutine
出现版本JDK 21(2023)Go 1.0(2012)
映射模型N:M(虚拟线程 → 平台线程)N:M(goroutine → OS 线程)
调度器ForkJoinPool(work-stealing)Go Scheduler(work-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 密集用虚拟线程,两者可以混用

并发编程的本质不是「跑得更快」,而是「等得更聪明」。虚拟线程把这个哲学贯彻到了极致。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/24 2:02:57

卵巢早衰备孕还有机会吗

卵巢早衰备孕还有机会吗&#xff1f;辅酶Q10的调理思路分享小雅今年32岁&#xff0c;例假一直不太规律&#xff0c;去年体检时AMH值只有0.8&#xff0c;医生说卵巢储备功能有下降趋势。拿到报告的那一刻&#xff0c;她哭了很久&#xff1a;"我才32岁&#xff0c;怎么就卵巢…

作者头像 李华
网站建设 2026/6/24 2:02:41

用 Typeoff 口述代码思路:从原始想法到结构化 Markdown

我用语音口述写完了上周所有的 PR 描述和 Bug 复盘——一份给开发者的 Typeoff 实战工作流利益相关声明: 本人 Typeoff 个人用户,使用约一个月。本文非官方稿件、无任何商业合作。Typeoff 功能描述以官方文档为准。文中提到的 Cursor、Claude Code、Wispr Flow 等均为公开可查的…

作者头像 李华
网站建设 2026/6/24 2:01:30

AVR单片机内部温度传感器校准指南:从原理到单点/两点校准实践

1. 项目概述&#xff1a;为什么AVR内部温度传感器需要校准&#xff1f; 如果你玩过AVR单片机&#xff0c;比如经典的ATmega328P&#xff08;Arduino Uno的核心&#xff09;或者ATtiny系列&#xff0c;你可能知道它们内部集成了一个温度传感器。这个功能听起来很酷&#xff0c;对…

作者头像 李华
网站建设 2026/6/24 2:00:25

XMEGA A3BU嵌入式开发实战:低功耗、高精度ADC与时钟系统深度优化

1. 项目概述&#xff1a;为什么XMEGA A3BU值得深挖&#xff1f;在嵌入式开发领域&#xff0c;尤其是对功耗、模拟信号采集和实时性有苛刻要求的应用里&#xff0c;选对微控制器&#xff08;MCU&#xff09;往往是项目成功的一半。今天我们不聊那些“网红”型号&#xff0c;而是…

作者头像 李华
网站建设 2026/6/24 1:56:26

ATtiny88 SPI与TWI通信接口:寄存器级配置与实战避坑指南

1. 项目概述&#xff1a;为什么ATtiny88的通信接口值得深挖&#xff1f;如果你玩过一阵子单片机&#xff0c;尤其是像ATtiny88这类小巧的AVR芯片&#xff0c;可能会觉得它就是个“小玩意儿”&#xff0c;资源有限&#xff0c;干不了什么大事。但恰恰是这种资源受限的环境&#…

作者头像 李华