引言依稀还记得2016年开始学Java的场景时光的距离是如此简短十年时间仿佛隔桌而坐。刚学java时用的还是jdk1.6jdk6到现在最新的jdk版本已经是25了在java圈子里有一个诙谐的说法来形容jdk的快速迭代“新版任你发我用java8你升随你升我用java8”这既是玩笑也是真实写照。就我个人而言使用的最多的也是jdk8在22年后新项目都升级到了jdk17今年的一个项目又开始使用jdk21而jdk21的一个重要特性便是正式引入了虚拟线程Virtual Thread。为什么要引入虚拟线程在jdk21之前Java 的线程模型平台线程 Platform Thread是一对一映射到操作系统线程OS Thread的。这带来了几个根本性问题OS 线程的代价很昂贵每个 OS 线程默认栈大小约 512KB~1MB线程创建/销毁涉及系统调用开销大上下文切换context switch由 OS 调度成本高一台普通服务器能稳定支撑的 OS 线程数通常只有几千个这也是网关、RPC、HTTP服务这类IO密集型服务的核心性能瓶颈。在jdk21中如何创建线程//创建平台线程 Thread.ofPlatform().start(() - System.out.println(platform thread)); //传统方式 - 创建平台线程 Thread t1 new Thread(() - System.out.println(platform thread)); t1.start(); //创建虚拟线程 Thread vt Thread.ofVirtual().start(() - System.out.println(virtual thread));虚拟线程的原理结合Linux中的内核线程Kernel Thread和用户线程User Thread的关系更容易理解虚拟线程的原理。Linux 中线程分两个层面内核线程Kernel Thread 由 OS 内核管理真正在 CPU 上执行是 OS 调度的基本单位。用户线程User Thread 在用户空间实现内核不感知由用户空间的线程库管理调度。两者的映射关系历史上有三种模型1:1 模型一个用户线程对应一个内核线程Linux NPTL现代Linux的默认实现N:1 模型所有用户线程映射到一个内核线程内核完全不感知多线程M:N 模型M个用户线程映射到N个内核线程两级调度jdk21虚拟线程与平台线程的关系与 M:N 模型 类似核心思想都是用少量的“重”线程驱动大量的“轻”线程调度器把对应的内核线程/载体线程切换去执行其他任务避免资源浪费。阻塞时的处理思路也相同用户线程/虚拟线程阻塞时调度器把对应的内核线程/载体线程切换去执行其他任务避免资源浪费。但是两者又有着重要区别1、内核感知程度不同Linux 用户线程N:1或M:N内核完全不感知用户线程的存在 内核只看到内核线程不知道上面跑了多少用户线程 Java 虚拟线程载体线程是真实的 OS 线程内核完全感知载体线程 虚拟线程本身确实对内核不可见但载体线程对内核完全可见2、阻塞处理机制不同这是最关键的区别Linux 用户线程阻塞时的问题 用户线程发起系统调用如 read()→ 内核线程被阻塞 → 整个内核线程卡住 → 该内核线程上的所有用户线程都无法执行 → 这是 N:1 模型的致命缺陷 Java 虚拟线程阻塞时 虚拟线程发起 I/O → JVM 拦截改写为异步 I/O → 虚拟线程从载体线程卸载 → 载体线程立刻去执行其他虚拟线程 → I/O 完成后虚拟线程重新挂载Java 虚拟线程能解决阻塞问题根本原因是 JVM 在底层把阻塞式 I/O 替换成了异步 I/O基于 Linux 的 io_uring 或 epoll这是 Linux 原始用户线程库做不到的事情。3、历史背景不同Linux 的 M:N 模型如早期的 NGPT最终被放弃了现代 Linux 默认使用 1:1 模型NPTL原因是 M:N 实现复杂、调试困难、性能不稳定。Java 虚拟线程建立在现代 OS 已经非常成熟的异步 I/O 基础上站在了更高的起点规避了当年 M:N 模型的主要问题。4、调度器位置不同Linux M:N调度器在用户空间的线程库中如 libpthread Java 虚拟线程调度器在 JVM 内部ForkJoinPool且 JVM 可以感知 所有的 I/O 操作在恰当时机主动触发卸载java 虚拟线程与 Linux M:N 用户线程是同一思想的不同实现。核心区别在于Java 虚拟线程依托 JVM 对 I/O 的全面拦截和异步化改造真正解决了阻塞穿透问题而这正是当初 Linux M:N 模型最终失败的根本原因。用下图可以总结Linux 历史模型 N:1 用户线程A ──┐ 用户线程B ──┼──→ 内核线程1 ← 一个阻塞全部卡死 用户线程C ──┘ M:N 用户线程A ──→ 内核线程1 用户线程B ──→ 内核线程2 ← 理想但实现复杂已被放弃 用户线程C ──┘ 1:1 用户线程A ──→ 内核线程A ← 现代Linux默认简单但线程数受限 用户线程B ──→ 内核线程B Java 虚拟线程本质是改良的M:N 虚拟线程A ──→ 载体线程1OS线程 虚拟线程B ──┘ ↑ 阻塞时自动卸载靠JVM拦截I/O实现 虚拟线程C ──→ 载体线程2OS线程 虚拟线程D ──┘虚拟线程背后的载体线程是什么虚拟线程的载体线程其实也就是平台线程Platform Thread平台线程对应真实的 OS 线程。使用如下代码测试private static Thread getCarrierThread(Thread virtualThread) throws Exception { // Java 21 中 VirtualThread 内部有 carrierThread 字段 Class? vtClass Class.forName(java.lang.VirtualThread); Field carrierField vtClass.getDeclaredField(carrierThread); carrierField.setAccessible(true); return (Thread) carrierField.get(virtualThread); } private static void createVirtualThread() { Thread vt Thread.ofVirtual().start(() - { try { Thread carrier getCarrierThread(Thread.currentThread()); System.out.println(虚拟线程: Thread.currentThread()); System.out.println(载体线程: carrier); } catch (Exception e) { e.printStackTrace(); } });通过main方法启动执行createVirtualThreadvm参数中增加--add-opens java.base/java.langALL-UNNAMED控制台输出日志如下:虚拟线程: VirtualThread[#21]/runnableForkJoinPool-1-worker-1 载体线程: Thread[#22,ForkJoinPool-1-worker-1,5,CarrierThreads]可以看出载体线程实际上是ForkJoinPool-1-worker-线程池中的线程1疯狂创建100个虚拟线程并打印:for (int i 0; i 100; i) { int finalI i; Thread vt2 Thread.ofVirtual().start(() - { try { Thread carrier getCarrierThread(Thread.currentThread()); System.out.println(虚拟线程 finalI : Thread.currentThread()); System.out.println(载体线程 finalI : carrier); } catch (Exception e) { e.printStackTrace(); } }); }输出结果如下:虚拟线程 0 : VirtualThread[#27]/runnableForkJoinPool-1-worker-2 载体线程 0 : Thread[#24,ForkJoinPool-1-worker-2,5,CarrierThreads] 虚拟线程 2 : VirtualThread[#29]/runnableForkJoinPool-1-worker-3 载体线程 2 : Thread[#26,ForkJoinPool-1-worker-3,5,CarrierThreads] 虚拟线程 6 : VirtualThread[#33]/runnableForkJoinPool-1-worker-3 虚拟线程 7 : VirtualThread[#34]/runnableForkJoinPool-1-worker-3 载体线程 7 : Thread[#26,ForkJoinPool-1-worker-3,5,CarrierThreads] 虚拟线程 8 : VirtualThread[#35]/runnableForkJoinPool-1-worker-3 载体线程 8 : Thread[#26,ForkJoinPool-1-worker-3,5,CarrierThreads] 虚拟线程 9 : VirtualThread[#36]/runnableForkJoinPool-1-worker-3 载体线程 9 : Thread[#26,ForkJoinPool-1-worker-3,5,CarrierThreads] 虚拟线程 10 : VirtualThread[#37]/runnableForkJoinPool-1-worker-3 载体线程 10 : Thread[#26,ForkJoinPool-1-worker-3,5,CarrierThreads] 虚拟线程 11 : VirtualThread[#38]/runnableForkJoinPool-1-worker-3 载体线程 11 : Thread[#26,ForkJoinPool-1-worker-3,5,CarrierThreads] ...可以确认虚拟线程默认使用的就是ForkJoinPool的一个线程池实例中的创建的线程。对java stream 并行流比较熟悉的同学会意识到这会与stream并行流所使用的线程池有线程复用吗答案是不会实际是两个ForkJoinPool实例。如下图所示stream并行流使用的线程池是ForkJoinPool.commonPool-worker-使用虚拟线程应该注意什么虚拟线程解决的是 I/O 等待造成的线程资源浪费问题让你用简单的同步代码写出高并发程序。什么情况下适合使用虚拟线程核心判断标准任务是 I/O 密集型线程大量时间在等待高并发 Web 服务每个 HTTP 请求一个虚拟线程支撑万级并发数据库访问大量 JDBC 查询等待数据库响应微服务间 HTTP 调用调用下游服务时的网络等待文件 I/O 操作读写磁盘的等待时间消息队列消费等待消息的长轮询需要从异步代码迁回同步代码将 reactive 代码简化重写简单说线程的大部分时间在 wait / 阻塞而不是在跑 CPU就适合用虚拟线程。什么情况下不适合使用虚拟线程1、CPU 密集型任务如图像处理、加密计算、机器学习推理、大量数学计算。虚拟线程不会让 CPU 跑得更快瓶颈是 CPU 核心数不是线程数。这种场景用 ForkJoinPool 或固定大小线程池反而更合适。2、线程数量本来就很少的场景如果你的应用本来只需要几十个线程传统线程完全够用引入虚拟线程没有收益。3、存在 synchronized 持有锁时发生阻塞Pinning 问题// ❌ 危险synchronized 块内有 I/O 阻塞 → pinning synchronized (lock) { result jdbcConnection.query(...); // 虚拟线程被 pin 住 } // ✅ 改用 ReentrantLock private final ReentrantLock lock new ReentrantLock(); lock.lock(); try { result jdbcConnection.query(...); // 虚拟线程可以正常卸载 } finally { lock.unlock(); }这是虚拟线程目前最重要的陷阱。当虚拟线程在 synchronized 块内部发生阻塞它无法从 carrier thread 卸载称为 pinningjdk24中已经解决退化成平台线程的行为甚至可能造成 carrier thread 耗尽。文章转载自DeepSky丶原文链接https://www.cnblogs.com/deep-sky/p/19656303体验地址http://www.jnpfsoft.com/?from001YH