ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

49. 【Java】JDK 21的虚拟线程(上):革命性的并发模型

49. 【Java】JDK 21的虚拟线程(上):革命性的并发模型 摘要本文深入解析JDK 21引入的虚拟线程Virtual Threads技术从传统平台线程的资源瓶颈出发阐述虚拟线程的轻量级特性、M:N调度模型及其工作原理。详细介绍了两种创建虚拟线程的方法并通过性能对比实验展示其在I/O密集型场景下的显著优势。总结了虚拟线程的适用场景、使用注意事项以及与平台线程的核心差异帮助开发者掌握这一革命性的并发编程工具。关键词虚拟线程, Java并发, JDK 21, I/O密集型, 线程池在之前的几篇文章里我们从“线程的创建”一步步学到了“JUC并发工具”。你知道了怎么用synchronized保护共享数据怎么用wait()/notify()让线程协作也知道了ExecutorService线程池和ConcurrentHashMap这些强大的工具。但是不知道你有没有隐隐感觉到一丝“不对劲”我们来做一个思想实验。假设你是一个 Web 服务的开发者你的服务需要同时处理 10,000 个用户的请求。按照传统的做法你会用线程池比如Executors.newFixedThreadPool(200)——最多同时处理 200 个请求剩下的请求在队列里排队。这 200 个线程每一个都对应着一个操作系统线程。每个操作系统线程需要大约1MB 的栈内存。200 个线程就是 200MB勉强还能接受。但如果是 10,000 个并发呢10GB 内存光是栈就把机器撑爆了。所以传统线程池的瓶颈在于线程的数量被操作系统限制了。即使 CPU 还有空闲你也没法创建更多的线程因为每个线程的内存开销太大了。但你想一想这 10,000 个用户请求大多数时间在干什么它们在等待数据库查询结果、等待调用下游 API、等待读取文件……这些 I/O 操作占用了绝大部分时间而线程在这段时间里是阻塞的——它什么事都没做却白白占着 1MB 的内存和宝贵的操作系统线程资源。这不就浪费了吗JDK 21 引入的虚拟线程Virtual Threads就是为了解决这个问题而生的。1. 虚拟线程是什么—— 由 JVM 管理的“轻量级线程”虚拟线程你可以理解为“由 JVM 管理的轻量级用户线程”。它和传统的“平台线程Platform Thread”有本质的区别平台线程直接映射到操作系统线程1:1 关系。创建、销毁、上下文切换都需要操作系统介入开销大。虚拟线程不直接映射到操作系统线程。JVM 在少量的平台线程上“调度”大量的虚拟线程M:N 关系。虚拟线程的创建和切换由 JVM 在用户态完成不涉及内核态切换。打个比方平台线程就像是“正式工”每个正式工都有自己独立的办公室1MB 栈内存招聘一个正式工要走 HR 流程、要审批、要分配办公室操作系统介入。而虚拟线程就像是“临时工”他们不占独立办公室而是在一个大的开放工位平台线程上轮流工作。当一个临时工在等咖啡I/O 阻塞时他立刻让出工位另一个临时工马上坐上来干活。虚拟线程的核心特性特性说明极致轻量每个虚拟线程只占几百字节到 160KB 的内存可以轻松创建数百万个用户态调度由 JVM 调度不经过操作系统内核切换开销极低自动挂起/恢复遇到 I/O 阻塞时自动“让出”载体线程I/O 完成后自动恢复兼容现有 API完全兼容java.lang.Thread和ExecutorServiceAPI2. 传统线程的“三座大山”在深入虚拟线程之前我们先来回顾一下传统平台线程的三大痛点① 资源消耗高每个平台线程需要分配独立的栈空间默认 1MB。1 万个线程就是 10GB 内存这在大多数服务器上都是不可接受的。② 调度开销大线程切换需要陷入操作系统内核态上下文切换的成本很高。当大量线程在阻塞和就绪之间频繁切换时CPU 大量的时间都花在了“切换”上而不是“干活”上。③ 扩展性瓶颈操作系统能支持的线程数量是有限的通常几千到几万。即使你的 CPU 有 64 个核心你也无法创建 10 万个平台线程。为了解决这些问题传统的做法是线程池——限制并发线程的数量让线程复用。但线程池也有它的代价你需要小心翼翼地调优池的大小池太小了吞吐量不够池太大了内存撑不住。而且如果一个线程在池中执行一个阻塞 I/O 操作它仍然会占着池中的一个位置导致其他任务排队等待。虚拟线程的思路完全不同不再限制线程的数量而是让线程变得极其轻量轻到你可以为每个任务创建一个线程。3. 虚拟线程的工作原理 —— “M:N”调度模型虚拟线程最核心的设计是M:N 调度模型M 个虚拟线程运行在 N 个平台线程上M 远大于 N。每个平台线程载体线程一次只能“承载”一个虚拟线程。当虚拟线程执行到阻塞操作比如Thread.sleep()、socket.read()、数据库查询时它会自动从载体线程上卸载载体线程立刻去执行另一个就绪的虚拟线程。当阻塞操作完成时虚拟线程会被重新调度到某个空闲的载体线程上继续执行。这意味着虚拟线程在阻塞时不会“浪费”平台线程资源。平台线程始终在忙碌地执行着某些虚拟线程的任务CPU 利用率被拉满。JVM 使用ForkJoinPool作为虚拟线程的默认调度器。它采用“工作窃取”算法让各个载体线程之间的负载保持均衡。4. 如何创建虚拟线程JDK 21 提供了两种主要方式来创建虚拟线程。4.1 方式一Thread.ofVirtual()这是最直接的方式和创建普通线程的语法几乎一样// 创建一个虚拟线程并立即启动ThreadvtThread.ofVirtual().start(()-{System.out.println(Hello from virtual thread: Thread.currentThread());});// 等待虚拟线程执行完毕vt.join();你还可以给虚拟线程起名字方便调试ThreadvtThread.ofVirtual().name(my-virtual-thread).start(()-{System.out.println(Running in: Thread.currentThread().getName());});vt.join();甚至可以用Thread.Builder来批量创建带编号的线程varbuilderThread.ofVirtual().name(worker-,0);Threadt1builder.start(()-System.out.println(Task 1));Threadt2builder.start(()-System.out.println(Task 2));t1.join();t2.join();// 输出worker-0 和 worker-14.2 方式二Executors.newVirtualThreadPerTaskExecutor()如果你需要提交大量任务使用虚拟线程的ExecutorService会更方便try(varexecutorExecutors.newVirtualThreadPerTaskExecutor()){// 提交 10,000 个任务每个任务都在一个新的虚拟线程中执行for(inti0;i10_000;i){inttaskIdi;executor.submit(()-{System.out.println(Task taskId running in Thread.currentThread());});}}// try-with-resources 会自动等待所有任务完成并关闭线程池newVirtualThreadPerTaskExecutor()会为每个提交的任务创建一个新的虚拟线程。它和newCachedThreadPool()类似——都是“来一个任务创建一个线程”——但区别在于它创建的是虚拟线程而不是平台线程所以你可以创建数百万个而不会耗尽系统资源。4.3 一个直观的性能对比我们来做一个简单的实验importjava.time.Duration;importjava.util.concurrent.Executors;importjava.util.stream.IntStream;publicclassVirtualThreadComparison{publicstaticvoidmain(String[]args){// 1. 虚拟线程每个任务一个虚拟线程longstart1System.currentTimeMillis();try(varexecutorExecutors.newVirtualThreadPerTaskExecutor()){IntStream.range(0,10_000).forEach(i-{executor.submit(()-{Thread.sleep(Duration.ofSeconds(1));returni;});});}System.out.println(虚拟线程耗时(System.currentTimeMillis()-start1)ms);// 2. 固定线程池20个线程任务排队执行longstart2System.currentTimeMillis();try(varexecutorExecutors.newFixedThreadPool(20)){IntStream.range(0,10_000).forEach(i-{executor.submit(()-{Thread.sleep(Duration.ofSeconds(1));returni;});});}System.out.println(固定线程池(20)耗时(System.currentTimeMillis()-start2)ms);}}在我的机器上虚拟线程版本在几秒内就能完成所有任务而固定线程池20 个线程需要大约 500 秒因为 10,000 个任务排队每次只能执行 20 个每个任务耗时 1 秒。这个对比直观地展示了虚拟线程在 I/O 密集型场景下的巨大优势。5. 虚拟线程适合什么场景✅ 适合I/O 密集型任务虚拟线程是 I/O 密集型任务的“天选之子”。包括Web 服务器处理 HTTP 请求微服务之间的 RPC 调用数据库查询JDBC文件读写网络通信在这些场景中线程大部分时间都在等待 I/O 完成。虚拟线程让这些等待时间不再“浪费”平台线程资源。❌ 不适合CPU 密集型任务如果你的任务需要大量的 CPU 计算比如视频编码、加密解密、大规模矩阵运算虚拟线程并不会带来性能提升。因为 CPU 密集型任务并不会阻塞虚拟线程不会“让出”载体线程它和普通线程在 CPU 计算上的表现没有区别。对于 CPU 密集型任务你应该使用平台线程并且线程数量不要超过 CPU 核心数。6. 虚拟线程的使用注意事项虽然虚拟线程用起来很简单但有几个地方需要特别注意6.1 不要池化虚拟线程虚拟线程的设计初衷就是“即用即毁”——创建成本极低用完就可以丢弃。把虚拟线程池化是反模式不仅没有收益还会增加复杂性。// ❌ 错误池化虚拟线程ExecutorServicepoolExecutors.newFixedThreadPool(10);for(inti0;i1000;i){pool.submit(()-{/* 使用虚拟线程不这是平台线程池 */});}// ✅ 正确每次创建新的虚拟线程try(varexecutorExecutors.newVirtualThreadPerTaskExecutor()){for(inti0;i1000;i){executor.submit(()-{/* 每个任务一个新虚拟线程 */});}}6.2 谨慎使用ThreadLocal虚拟线程的数量可能非常多百万级如果每个虚拟线程都使用ThreadLocal存储数据内存消耗会迅速膨胀。在虚拟线程中ThreadLocal的生命周期应该尽量短不要用来存储长生命周期的数据。6.3 注意synchronized的“固定”问题Pinning当一个虚拟线程进入synchronized代码块或方法时它会被“固定”在当前的载体线程上——即使它在同步块内阻塞也不会让出载体线程。这意味着在synchronized块内执行阻塞 I/O 操作会浪费载体线程资源。建议在虚拟线程中尽量使用ReentrantLock替代synchronized。ReentrantLock不会导致固定问题。// ❌ 可能导致固定synchronized(lock){// 这里执行阻塞 I/O 操作Thread.sleep(1000);}// ✅ 推荐使用 ReentrantLocklock.lock();try{Thread.sleep(1000);}finally{lock.unlock();}7. 虚拟线程 vs 平台线程 —— 一个直观的对比维度平台线程虚拟线程管理方操作系统JVM栈内存~1MB~160KB 或更少创建上限数千~数万数百万阻塞时行为占用操作系统线程资源自动让出载体线程上下文切换内核态开销大用户态开销极小适用场景CPU 密集型I/O 密集型编程模型需要线程池管理一任务一线程简单直观8. 今天的总结今天我们初步认识了 JDK 21 最重磅的特性——虚拟线程虚拟线程是什么由 JVM 管理的轻量级用户线程不直接映射到操作系统线程。为什么需要它传统平台线程资源消耗大、调度开销高、数量受限于操作系统。工作原理M:N 调度模型虚拟线程在阻塞时自动让出载体线程。如何创建Thread.ofVirtual().start()或Executors.newVirtualThreadPerTaskExecutor()。适用场景I/O 密集型任务Web 服务、微服务、数据库访问。注意事项不要池化虚拟线程、谨慎使用ThreadLocal、用ReentrantLock替代synchronized。虚拟线程让 Java 的并发编程进入了一个新时代。它让我们可以回归到最简单、最直观的并发模型——为每个任务创建一个线程而不用再为线程池的大小调优而烦恼。动手试试运行上面的性能对比代码在你的机器上测试虚拟线程和固定线程池处理 10,000 个任务每个任务Thread.sleep(1000)的耗时差异。用Thread.ofVirtual().start()创建 100,000 个虚拟线程每个线程打印一句话。观察你的机器是否还能流畅运行平台线程做不到这一点。写一个简单的 HTTP 服务器用虚拟线程处理每个请求提示用com.sun.net.httpserver.HttpServer每个请求分配一个虚拟线程。然后用压测工具如wrk或ab测试它的并发能力。尝试在虚拟线程中使用synchronized块执行一个阻塞操作观察是否有性能下降。然后用ReentrantLock替代对比两者的表现。思考如果你的项目是一个 Spring Boot 微服务如何启用虚拟线程提示Spring Boot 3.2 支持通过配置启用虚拟线程。下一篇文章我们将深入虚拟线程的实战与最佳实践包括如何从传统线程池迁移到虚拟线程、结构化并发的概念、以及虚拟线程的调试与监控。我们下一篇见。 获取本系列示例代码请访问 GitCode。
返回列表