当前位置: 首页 > news >正文

进程、线程、协程与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 密集用虚拟线程,两者可以混用

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

http://www.cnnetsun.cn/news/2994755.html

相关文章:

  • Dify、Cursor、Chatbox、Cherry Studio 怎么统一接入:Base URL、模型 ID 和 API Key 验收清单
  • 卵巢早衰备孕还有机会吗
  • 用 Typeoff 口述代码思路:从原始想法到结构化 Markdown
  • AVR单片机内部温度传感器校准指南:从原理到单点/两点校准实践
  • XMEGA A3BU嵌入式开发实战:低功耗、高精度ADC与时钟系统深度优化
  • ATtiny88 SPI与TWI通信接口:寄存器级配置与实战避坑指南
  • 嵌入式安全芯片HAL层开发指南:从CryptoAuthLib原理到STM32实战
  • ATmega单片机端口复用:从GPIO到SPI/ADC/中断的实战配置与冲突解决
  • ATmega164P/324P/644P嵌入式实战:选型、低功耗与汽车级应用
  • CD5283/CD5314电流调节器芯片:从恒流原理到PCB设计实战
  • 备孕期为什么要补充维生素b?高仕星维生素b帮你打好营养基础
  • Curiosity Nano Base硬件平台:标准化连接如何提升嵌入式开发效率
  • ARM7TDMI编程模型与Thumb指令集:嵌入式开发的底层基石
  • 天峰律政代表的合规公关派正在重塑行业服务标准
  • KA Music:酷狗概念版纯净轻量替代品,无损音质免费畅听
  • Go语言的sync.Map条件操作
  • 理解「数据网格」(Data Mesh)及其对数据平台架构的影响
  • 分布式系统一致性算法详解
  • 软件直方图管理化的分布分析
  • khmer开发者手册:贡献代码与扩展功能的完整流程
  • SongGeneration:用AI技术让音乐创作触手可及
  • 从零开始构建高效知识库:OB_Template模板库完整指南
  • Winboat启动故障深度解析:5种常见场景与高效解决方案
  • 构建企业级智能知识引擎:WeKnora RAG架构深度解析与部署实践
  • 3步快速修复BMS锁定电池:Open Battery Information终极指南
  • 终极PT助手:PT-Plugin-Plus浏览器插件完整使用指南
  • 如何为Newton物理引擎定制渲染管线:从原理到实战
  • 3分钟搞定M3U8下载:Fluent M3U8让你的视频保存如此简单
  • Gitnuro终极指南:跨平台Git客户端快速上手教程
  • 终极console-powers样式系统完全指南:10个技巧打造彩色控制台输出