<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>并发 on Renxin's Blog</title><link>https://zh.renxinblog.cn/tags/%E5%B9%B6%E5%8F%91/</link><description>Recent content in 并发 on Renxin's Blog</description><generator>Hugo -- gohugo.io</generator><language>zh</language><lastBuildDate>Thu, 13 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://zh.renxinblog.cn/tags/%E5%B9%B6%E5%8F%91/index.xml" rel="self" type="application/rss+xml"/><item><title>理解虚拟线程：从一次阻塞说起</title><link>https://zh.renxinblog.cn/post/java/understanding-virtual-threads/</link><pubDate>Thu, 13 Aug 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/java/understanding-virtual-threads/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/java-understanding-virtual-threads-cover.png" alt="Featured image of post 理解虚拟线程：从一次阻塞说起" /&gt;&lt;p&gt;你有没有遇到过这种情况——&lt;/p&gt;
&lt;p&gt;一个典型的 Java Web 服务，Tomcat 默认线程池 200。请求处理逻辑很简单：查一下数据库，调一个远程接口，返回结果。每个请求从进入线程池到返回，大部分时间都在等数据库查询、等远程响应。&lt;/p&gt;
&lt;p&gt;并发一上来，问题就出现了：200 个线程很快全部阻塞在 IO 等待上，新请求全部排队。&lt;/p&gt;
&lt;p&gt;这时候你多半会想：线程池不够，加线程不就行了？&lt;/p&gt;
&lt;p&gt;试了才知道这条路走不通。加到 1000，确实多撑了一会儿——但 1000 个线程照样会阻塞在 IO 上，第 1001 个请求照样排队。想靠加线程支撑真正的并发量？几万并发需要几万线程，每个线程 1MB 栈，就是几十 GB 内存；就算内存扛得住，操作系统调度几万个线程的开销也会把 CPU 吃光。&lt;/p&gt;
&lt;p&gt;于是你看到一种奇怪的状况：CPU 大多是空闲的——因为线程都在等 IO，没有计算在跑；内存也没满——因为当前只有 200 个线程在跑，只占了 200MB 栈。但并发就是上不去，而且&lt;strong&gt;加线程这条路本身是死路&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这不是硬件问题，是&lt;strong&gt;线程模型&lt;/strong&gt;问题：线程是稀缺的「重」资源，一个线程被 IO 阻塞时，它什么都没干，却把一个宝贵的线程占住了。而 JDK 21 的虚拟线程，就是来改这个的。&lt;/p&gt;
&lt;p&gt;这篇文章用一条问题链讲透虚拟线程：平台线程为什么不够用 → 虚拟线程怎么解决 → 它适合什么、不适合什么。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;%%{init: {'theme':'base'}}%%
graph LR
 A["核心结论：&lt;br/&gt;虚拟线程让 Java 支持百万级并发"] --&gt; B["平台线程&lt;br/&gt;为什么不够用"]
 A --&gt; C["虚拟线程&lt;br/&gt;怎么解决"]
 A --&gt; D["边界&lt;br/&gt;适合什么"]
 style A fill:#d1fae5,stroke:#059669,color:#064e3b
 style B fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style C fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style D fill:#dbeafe,stroke:#2563eb,color:#1e3a5f&lt;/pre&gt;&lt;h2 id="平台线程的四个瓶颈"&gt;平台线程的四个瓶颈
&lt;/h2&gt;&lt;p&gt;Java 的传统线程（平台线程）和操作系统线程是&lt;strong&gt;一一对应&lt;/strong&gt;的：Java 代码每创建一个线程，JVM 就调用操作系统 API 创建一个 OS 线程。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;%%{init: {'theme':'base'}}%%
graph LR
 A["Java 线程"] --&gt; B["OS 线程"]
 B --&gt; C["内核调度&lt;br/&gt;1:1 映射"]
 style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style B fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style C fill:#fef9c3,stroke:#ca8a04,color:#713f12&lt;/pre&gt;&lt;p&gt;这种 1:1 模型在 Java 诞生时没问题，但放到现代高并发场景，四个瓶颈逐个暴露。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;瓶颈&lt;/th&gt;
					&lt;th&gt;表现&lt;/th&gt;
					&lt;th&gt;根源&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;创建销毁贵&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;线程创建销毁涉及内核态切换&lt;/td&gt;
					&lt;td&gt;要操作系统参与&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;内存占用大&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;每个线程默认分配 1MB 栈空间&lt;/td&gt;
					&lt;td&gt;JVM 为每个平台线程预留栈&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;上下文切换贵&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;保存恢复大量寄存器、栈状态，CPU 缓存失效&lt;/td&gt;
					&lt;td&gt;内核调度，涉及用户态/内核态切换&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;可用数量有限&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;一个进程通常只能创建数千个线程&lt;/td&gt;
					&lt;td&gt;内存和调度成本双重限制&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;一句话总结：&lt;strong&gt;平台线程是「重」资源&lt;/strong&gt;，不能随便创建。所以 Java 应用被迫用线程池复用线程——但线程池也有上限，并发一高就排队。&lt;/p&gt;
&lt;p&gt;这就是为什么现代语言纷纷引入轻量线程：Go 的 goroutine、Kotlin 的协程。Java 直到 JDK 21 才正式端出虚拟线程（JEP 444，2023 年 9 月 19 日发布）。&lt;/p&gt;
&lt;h2 id="虚拟线程用户态轻量级线程"&gt;虚拟线程：用户态轻量级线程
&lt;/h2&gt;&lt;p&gt;虚拟线程和平台线程最大的区别：&lt;strong&gt;它不由操作系统管理，由 JVM 管理&lt;/strong&gt;。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;平台线程&lt;/th&gt;
					&lt;th&gt;虚拟线程&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;创建销毁&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;内核态，贵&lt;/td&gt;
					&lt;td&gt;用户态，JVM 完成，便宜&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;内存占用&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;默认 1MB 栈&lt;/td&gt;
					&lt;td&gt;几 KB 级&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;上下文切换&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;内核调度，保存大量状态&lt;/td&gt;
					&lt;td&gt;用户态，保存少量状态&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;可用数量&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;数千级&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;数百万级&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;数字对比很直观：平台线程 1MB 栈，虚拟线程只要几 KB——&lt;strong&gt;内存开销差两个数量级&lt;/strong&gt;。这也是为什么虚拟线程能支撑「thousands or millions」级别的并发，而平台线程到几千就顶不住了。&lt;/p&gt;
&lt;p&gt;但虚拟线程的核心价值不只是「轻」，而是它的&lt;strong&gt;调度机制&lt;/strong&gt;——这才是它和 goroutine 一样优雅的地方。&lt;/p&gt;
&lt;h2 id="虚拟线程如何工作阻塞就挂起"&gt;虚拟线程如何工作：阻塞就挂起
&lt;/h2&gt;&lt;p&gt;理解虚拟线程，先理解一个词：&lt;strong&gt;carrier（载体线程）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;JVM 底层有一组平台线程专门用来运行虚拟线程（数量默认等于可用 CPU 核数），虚拟线程被调度器分配到这组 carrier 上执行。调度器是 JDK 内置的 work-stealing ForkJoinPool，FIFO 模式。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;%%{init: {'theme':'base'}}%%
graph LR
 A["虚拟线程 1"] --&gt; C["carrier 平台线程&lt;br/&gt;（调度器分配）"]
 B["虚拟线程 2"] --&gt; C
 D["虚拟线程 3"] --&gt; C
 style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style B fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style D fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style C fill:#fef9c3,stroke:#ca8a04,color:#713f12&lt;/pre&gt;&lt;p&gt;关键机制在阻塞发生时。看这个流程：&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;%%{init: {'theme':'base'}}%%
graph LR
 A["虚拟线程执行&lt;br/&gt;调用阻塞 I/O"] --&gt; B["JVM 识别阻塞调用&lt;br/&gt;自动执行非阻塞 OS 调用"]
 B --&gt; C["虚拟线程挂起&lt;br/&gt;释放 carrier"]
 C --&gt; D["carrier 去运行&lt;br/&gt;其他虚拟线程"]
 D --&gt; E["I/O 完成&lt;br/&gt;调度器恢复虚拟线程"]
 E --&gt; A
 style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style B fill:#d1fae5,stroke:#059669,color:#064e3b
 style C fill:#fef9c3,stroke:#ca8a04,color:#713f12
 style D fill:#d1fae5,stroke:#059669,color:#064e3b
 style E fill:#d1fae5,stroke:#059669,color:#064e3b&lt;/pre&gt;&lt;p&gt;当虚拟线程调用阻塞 I/O 时，JVM 运行时&lt;strong&gt;自动把阻塞调用替换成非阻塞 OS 调用&lt;/strong&gt;，然后挂起虚拟线程、释放 carrier。carrier 立刻去运行别的虚拟线程，不空等。&lt;/p&gt;
&lt;p&gt;对比平台线程：一个平台线程阻塞在数据库查询或远程调用上，它只能空等，线程池里这个位置就被白白占用。200 个请求并发，200 个线程全在等 IO，第 201 个请求只能排队——这就是开篇那个场景。&lt;/p&gt;
&lt;p&gt;JEP 444 把这层意思说得很直白：&lt;strong&gt;线程数往往在 CPU 或网络连接等资源耗尽之前，就先成为限制因素&lt;/strong&gt;（&amp;ldquo;the number of threads often becomes the limiting factor long before other resources, such as CPU or network connections, are exhausted&amp;rdquo;）。阻塞的线程不消耗 CPU，但占着线程名额——所以你会看到资源有余、并发却上不去的怪象。&lt;/p&gt;
&lt;p&gt;虚拟线程的思路是：&lt;strong&gt;我阻塞，但不占着茅坑&lt;/strong&gt;。挂起时不消耗 carrier 资源，恢复后继续执行。这就是「thread-per-request」风格能跑通百万并发的根本原因——每个请求一个虚拟线程，阻塞时自动让位。&lt;/p&gt;
&lt;h3 id="调度开销虚拟线程省下的第二个大头"&gt;调度开销：虚拟线程省下的第二个大头
&lt;/h3&gt;&lt;p&gt;上面讲的是「阻塞时不占线程」，虚拟线程还省下另一笔大账——&lt;strong&gt;调度开销&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;平台线程的调度由操作系统完成：切换线程时，内核要保存/恢复寄存器和栈状态，还可能导致 CPU 缓存失效，这是一笔昂贵的开销。线程越多，OS 花在调度上的时间比例越高，真正干活的 CPU 时间反而变少——&lt;strong&gt;这是平台线程并发数上不去的第二个原因&lt;/strong&gt;，和内存占用并列。&lt;/p&gt;
&lt;p&gt;虚拟线程的调度完全不同：它由 JVM 的 ForkJoinPool 调度到 carrier 上运行，&lt;strong&gt;挂起和恢复都在用户态完成，只保存/恢复少量状态&lt;/strong&gt;，不触发 OS 级上下文切换。JEP 444 原话是虚拟线程「cheap to create and almost infinitely plentiful」（创建便宜，数量近乎无限）。&lt;/p&gt;
&lt;p&gt;所以虚拟线程提高并发是&lt;strong&gt;双管齐下&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;成本维度&lt;/th&gt;
					&lt;th&gt;平台线程&lt;/th&gt;
					&lt;th&gt;虚拟线程&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;内存成本&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;1MB 栈/线程，几万线程就几十 GB&lt;/td&gt;
					&lt;td&gt;几 KB/线程，支持百万级&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;调度成本&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;OS 上下文切换，保存大量状态，缓存失效&lt;/td&gt;
					&lt;td&gt;JVM 用户态挂起/恢复，保存少量状态&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;内存和调度两个大头都降下来，并发数才能从「数千」跳到「数百万」。这也是为什么说虚拟线程是&lt;strong&gt;低成本提高并发&lt;/strong&gt;的手段——不是堆硬件，而是把每个并发单元的代价做小。&lt;/p&gt;
&lt;h2 id="虚拟线程的边界不是万能药"&gt;虚拟线程的边界：不是万能药
&lt;/h2&gt;&lt;p&gt;虚拟线程很强大，但有两个边界必须知道。&lt;/p&gt;
&lt;h3 id="边界一cpu-密集型任务没优势"&gt;边界一：CPU 密集型任务没优势
&lt;/h3&gt;&lt;p&gt;虚拟线程解决的瓶颈是「大量任务等待 I/O」。对 CPU 密集型任务——任务本身在计算，不阻塞——虚拟线程没有帮助。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;场景&lt;/th&gt;
					&lt;th style="text-align: center"&gt;虚拟线程效果&lt;/th&gt;
					&lt;th&gt;原因&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;IO 密集型&lt;/strong&gt;（请求等待数据库/网络/文件）&lt;/td&gt;
					&lt;td style="text-align: center"&gt;✅ 巨大提升&lt;/td&gt;
					&lt;td&gt;阻塞期释放 carrier，并发数提升几个数量级&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;CPU 密集型&lt;/strong&gt;（计算、加密、图像处理）&lt;/td&gt;
					&lt;td style="text-align: center"&gt;❌ 无优势&lt;/td&gt;
					&lt;td&gt;瓶颈是 CPU 核心数，不是线程数&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;CPU 密集场景的正确做法还是：合理分配任务到 CPU 核心，控制线程数。&lt;/p&gt;
&lt;h3 id="边界二pinning虚拟线程被钉住"&gt;边界二：pinning——虚拟线程被「钉住」
&lt;/h3&gt;&lt;p&gt;正常情况下，虚拟线程阻塞时释放 carrier。但如果虚拟线程执行到 &lt;strong&gt;synchronized 块或 native 方法&lt;/strong&gt; 时发生阻塞，它会被「钉」在 carrier 上——因为 JVM 无法在这些场景下安全地挂起和切换。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;%%{init: {'theme':'base'}}%%
graph LR
 A["虚拟线程执行&lt;br/&gt;synchronized 块"] --&gt; B["发生阻塞"]
 B --&gt; C["被钉在 carrier 上&lt;br/&gt;carrier 无法释放"]
 C --&gt; D["carrier 被占用&lt;br/&gt;其他虚拟线程排队"]
 style A fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
 style B fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
 style C fill:#fef9c3,stroke:#ca8a04,color:#713f12
 style D fill:#fee2e2,stroke:#dc2626,color:#7f1d1d&lt;/pre&gt;&lt;p&gt;这不会导致应用错误（JEP 444 明确说 pinning 不使应用 incorrect），但会&lt;strong&gt;阻碍扩展性&lt;/strong&gt;——被钉住的 carrier 不能服务其他虚拟线程。&lt;/p&gt;
&lt;p&gt;高频遇到 pinning 的场景：&lt;strong&gt;基于 JDBC 的数据库驱动&lt;/strong&gt;（MySQL Connector/J、PostgreSQL JDBC 等）和 &lt;strong&gt;synchronized 方法&lt;/strong&gt;。这也是为什么「虚拟线程 + 传统 JDBC 连接池」不是好组合——数据库连接是稀缺物理资源，连接池大小远小于虚拟线程并发数，大量虚拟线程会阻塞在等连接上，而 synchronized 又可能引发 pinning。&lt;/p&gt;
&lt;h2 id="回到开头"&gt;回到开头
&lt;/h2&gt;&lt;p&gt;回顾整条问题链：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;平台线程 1:1 映射 OS → 重资源 → 并发受限于线程数
 → 虚拟线程：用户态轻量级线程 → 阻塞就挂起，不占 carrier
 → IO 密集型受益巨大（百万级并发）
 → CPU 密集型无优势
 → synchronized/native 场景可能 pinning
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;下次遇到 Java 服务并发上不去，先想清楚两件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;任务是 IO 密集还是 CPU 密集？&lt;/strong&gt; IO 密集才考虑虚拟线程，CPU 密集先优化计算本身&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码里有没有 synchronized / JDBC 阻塞？&lt;/strong&gt; 有的话，先解决 pinning 场景再上虚拟线程&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;虚拟线程不是银弹，但它确实把 Java 带进了「百万级并发」的赛道——前提是用对场景。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;本文基于 JEP 444（openjdk.org/jeps/444）官方规范与 Java 版本历史（Wikipedia）的梳理，平台线程默认栈 1MB、虚拟线程数千万级等数据均来自该来源。&lt;/em&gt;&lt;/p&gt;</description></item></channel></rss>