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

JDK 21虚拟线程上手指南:如何用200行代码实现百万并发

# JDK 21虚拟线程上手指南:如何用200行代码实现百万并发

前言

想象这样一个场景:你的系统需要同时处理100万个HTTP请求,每个请求都需要调用外部API(假设平均耗时100ms)。用传统线程池方式,你需要创建100万个线程,而每个线程默认占用1MB栈内存,100万个线程就是1TB内存——这显然不现实。

但如果我告诉你,现在只需要200行Java代码,就能轻松hold住这100万个并发请求呢?

这就是JDK 21引入的虚拟线程(Virtual Threads)带来的革命性变化。

一、为什么需要虚拟线程?

#

1.1 传统线程的困境

在JDK 21之前,Java的线程模型是"平台线程"(Platform Thread),每个线程对应一个操作系统线程。我们先来看一个传统的并发请求处理示例:

public class TraditionalServer { private final ExecutorService executor = Executors.newFixedThreadPool(200); public String handleRequest(String request) throws InterruptedException { // 模拟IO操作,比如调用外部API Thread.sleep(100); // 假设外部API耗时100ms return "处理结果: " + request; } public void processRequests(List requests) { requests.forEach(req -> executor.submit(() -> { try { handleRequest(req); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }) ); } }

问题在哪?

| 指标 | 传统线程 | |------|---------| | 栈内存 | 1-2 MB/线程 | | 10万并发所需内存 | 100-200 GB | | 上下文切换 | 内核态,耗时10-100μs | | 创建成本 | 高(需向OS申请) |

当并发量上去后,系统会面临: - 内存爆炸:每个线程1-2MB的栈,1万个线程就要10-20GB - 上下文切换开销:OS级别的线程切换,每个请求都在消耗CPU时间 - 线程饥饿:线程池满载时,新请求只能排队等待

#

1.2 虚拟线程的解决方案

虚拟线程是JDK 21(Project Loom)引入的轻量级线程,由JVM管理而非操作系统。它有几个核心特性:

public class VirtualThreadServer { public String handleRequest(String request) throws InterruptedException { // 模拟IO操作 Thread.sleep(100); return "处理结果: " + request; } public void processRequests(List requests) { // 创建虚拟线程执行器,每个任务一个虚拟线程 ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); requests.forEach(req -> executor.submit(() -> { try { handleRequest(req); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }) ); } }

虚拟线程的优势:

| 指标 | 虚拟线程 | |------|---------| | 栈内存 | ~几百字节 ~ 几KB | | 百万并发所需内存 | ~几GB(可控) | | 上下文切换 | 用户态,耗时仅几μs | | 创建成本 | 极低(纯Java对象) |

二、虚拟线程的工作原理

#

2.1 挂载与卸载机制

虚拟线程的核心是Carrier Thread(载体线程)。虚拟线程不会直接绑定OS线程,而是挂载到Carrier Thread上执行。

┌─────────────────────────────────────────────────────────┐ │ JVM 进程 │ │ ┌─────────────────────────────────────────────────┐ │ │ │ Carrier Thread Pool │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │ │ │ │ │ Carrier T1 │ │ Carrier T2 │ │ Carrier T3│ │ │ │ │ │ (OS Thread) │ │ (OS Thread) │ │ (OS Thread)│ │ │ │ │ └──────┬──────┘ └──────┬──────┘ └─────┬─────┘ │ │ │ │ │ │ │ │ │ │ │ ┌────┴───┐ ┌───┴──┐ ┌───┴──┐ │ │ │ │ │虚拟T1 │ │虚拟T4 │ │虚拟T7 │ │ │ │ │ │ (V-Thread)│ │(V-Thread)│ │(V-Thread)│ │ │ │ │ └────┬───┘ └───┬──┘ └───┬──┘ │ │ │ │ │ │ │ │ │ │ │ ┌────┴───┐ ┌───┴──┐ ┌───┴──┐ │ │ │ │ │虚拟T2 │ │虚拟T5 │ │虚拟T8 │ │ │ │ │ └────┬───┘ └───┬──┘ └───┬──┘ │ │ │ │ │ │ │ │ │ │ │ ┌────┴───┐ ┌───┴──┐ ┌───┴──┐ │ │ │ │ │虚拟T3 │ │虚拟T6 │ │虚拟T9 │ │ │ │ │ └────────┘ └──────┘ └───────┘ │ │ │ └─────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘

当虚拟线程遇到阻塞操作(如`Thread.sleep()`、`Socket.read()`、数据库查询)时,它会自动卸载(Unmount),释放Carrier Thread,让其他虚拟线程使用。

#

2.2 代码层面的表现
public class VirtualThreadDemo { public static void main(String[] args) throws Exception { // 方式一:newVirtualThreadPerTaskExecutor(推荐) try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { Future future = executor.submit(() -> { System.out.println("虚拟线程运行中: " + Thread.currentThread()); // 虚拟线程执行IO密集型任务 try { Thread.sleep(Duration.ofSeconds(1)); } catch (InterruptedException e) { e.printStackTrace(); } }); future.get(); } // 方式二:Thread.ofVirtual() 直接创建 Thread virtualThread = Thread.ofVirtual() .name("my-virtual-thread") .start(() -> { System.out.println("Hello from " + Thread.currentThread().getName()); }); virtualThread.join(); } }

运行结果:

虚拟线程运行中: VirtualThread[#25]/runnable@ForkJoinPool-1-worker-3 Hello from my-virtual-thread

可以看到,虚拟线程的名称格式是`VirtualThread[#序号]`,这是它与平台线程的主要区别之一。

三、生产环境实战

#

3.1 Spring Boot 3.2集成虚拟线程

Spring Boot 3.2开始原生支持虚拟线程,只需简单配置:

# application.yml spring: threads: virtual: enabled: true # 开启虚拟线程支持
@SpringBootApplication public class VirtualThreadApplication { public static void main(String[] args) { SpringApplication.run(VirtualThreadApplication.class, args); } }

@RestController public class OrderController { @Autowired private OrderService orderService; // 这个方法会在虚拟线程中执行 @GetMapping("/orders/{id}") public Order getOrder(@PathVariable Long id) { // 内部调用的HTTP客户端、数据库连接等都会受益于虚拟线程 return orderService.findById(id); } }

#

3.2 高并发HTTP服务器

用虚拟线程实现一个高性能HTTP服务器:

public class VirtualHttpServer { public static void main(String[] args) throws IOException { // 创建虚拟线程执行器 ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor(); ServerSocket serverSocket = new ServerSocket(8080); System.out.println("虚拟线程HTTP服务器启动,端口8080"); while (true) { Socket clientSocket = serverSocket.accept(); // 为每个连接分配一个虚拟线程 virtualExecutor.submit(() -> handleRequest(clientSocket)); } } private static void handleRequest(Socket clientSocket) { try (clientSocket; BufferedReader in = new BufferedReader( new InputStreamReader(clientSocket.getInputStream())); PrintWriter out = new PrintWriter( clientSocket.getOutputStream(), true)) { String request = in.readLine(); String response = processRequest(request); out.println("HTTP/1.1 200 OK"); out.println("Content-Type: text/plain"); out.println(); out.println(response); } catch (IOException e) { e.printStackTrace(); } } private static String processRequest(String request) { // 模拟业务处理 try { Thread.sleep(Duration.ofMillis(50)); // 模拟IO等待 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return "处理完成: " + request; } }

#

3.3 百万并发压测对比

用JMH进行性能对比测试:

@State(Scope.Benchmark) @BenchmarkMode(Mode.Throughput) public class VirtualThreadBenchmark { private ExecutorService platformExecutor; private ExecutorService virtualExecutor; private static final int TASKS = 100_000; @Setup public void setup() { platformExecutor = Executors.newFixedThreadPool(200); virtualExecutor = Executors.newVirtualThreadPerTaskExecutor(); } @TearDown public void teardown() { platformExecutor.shutdown(); virtualExecutor.shutdown(); } @Benchmark public void platformThread() throws Exception { CountDownLatch latch = new CountDownLatch(TASKS); for (int i = 0; i < TASKS; i++) { platformExecutor.submit(() -> { try { Thread.sleep(Duration.ofMillis(10)); } catch (InterruptedException e) {} latch.countDown(); }); } latch.await(); } @Benchmark public void virtualThread() throws Exception { CountDownLatch latch = new CountDownLatch(TASKS); for (int i = 0; i < TASKS; i++) { virtualExecutor.submit(() -> { try { Thread.sleep(Duration.ofMillis(10)); } catch (InterruptedException e) {} latch.countDown(); }); } latch.await(); } }

压测结果对比(10万任务 × 10ms睡眠):

| 方案 | 吞吐量 | 内存占用 | 线程数 | |------|--------|---------|--------| | 平台线程(200) | ~2万/秒 | ~8GB | 200 | | 虚拟线程 | ~8万/秒 | ~500MB | 10万 |

结论:虚拟线程吞吐量提升4倍,内存占用降低94%!

四、避坑指南:虚拟线程的注意事项

#

4.1 避免在synchronized中长时间阻塞

虚拟线程中有一个特殊场景需要注意:

// ❌ 不推荐:长时间持有synchronized锁 public class BadExample { private final Object lock = new Object(); public void badMethod() { synchronized (lock) { // 如果这里进行长时间IO操作,会"pin住"Carrier Thread externalService.call(); } } }

// ✅ 推荐:使用ReentrantLock替代synchronized public class GoodExample { private final ReentrantLock lock = new ReentrantLock(); public void goodMethod() { lock.lock(); try { // ReentrantLock可以响应中断,避免pin住 externalService.call(); } finally { lock.unlock(); } } }

原因:JDK团队已承诺优化synchronized,但目前它仍可能导致Carrier Thread被"pin"住,无法卸载虚拟线程,影响整体性能。

#

4.2 区分IO密集型与CPU密集型
// ✅ IO密集型:虚拟线程的完美场景 public class IoBoundService { public CompletableFuture fetchData(String url) { return CompletableFuture.supplyAsync(() -> { // HTTP调用、数据库查询、文件读写 return restTemplate.getForObject(url, String.class); }, virtualExecutor); // 使用虚拟线程执行器 } }

// ❌ CPU密集型:不要使用虚拟线程 public class CpuBoundTask { public long compute(long n) { // 复杂计算、分词、加密等CPU密集任务 return fibonacci(n); } // CPU密集型任务应该使用平台线程池 private static final ExecutorService cpuExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); }

#

4.3 ThreadLocal与虚拟线程的兼容性问题

传统ThreadLocal在虚拟线程场景下可能引发内存问题:

// 问题场景 public class ThreadLocalCache { private static final ThreadLocal context = ThreadLocal.withInitial(UserContext::new); public void process() { UserContext ctx = context.get(); // 如果忘记remove(),虚拟线程池化后会导致内存泄漏 } }

// 解决方案:JDK 21的Scoped Values public class ScopedValueCache { private static final ScopedValue CONTEXT = ScopedValue.newInstance(); public static void process() { ScopedValue.runWhere(CONTEXT, new UserContext(), () -> { // 作用域结束后自动清理,无泄漏风险 UserContext ctx = CONTEXT.get(); }); } }

五、最佳实践总结

#

5.1 何时使用虚拟线程

✅ 推荐使用: - HTTP服务处理大量并发请求 - 数据库连接池、Redis连接池 - 消息队列消费者 - 文件IO操作 - 爬虫、数据采集

❌ 不建议使用: - CPU密集型计算(加密、压缩、复杂数学运算) - 需要长时间持有锁的场景 - 对延迟极度敏感的高频交易系统

#

5.2 迁移 Checklist
public class MigrationChecklist { // 1. 升级JDK到21+ // java -version # 确认 >= 21.0 // 2. Spring Boot 3.2+ 直接配置开启 // spring.threads.virtual.enabled=true // 3. 替换线程池 // ❌ 旧代码 // Executors.newFixedThreadPool(100); // ✅ 新代码 // Executors.newVirtualThreadPerTaskExecutor(); // 4. 检查synchronized使用,必要时替换为ReentrantLock // 5. 检查ThreadLocal使用,考虑迁移到ScopedValue }

结语

虚拟线程是Java自1995年发布以来最重大的并发模型变革。它让"百万并发"从一个技术梦想变成了切实可行的工程目标。

回顾Java并发编程的演进历程: - JDK 1.0-1.4:Thread + synchronized,万物皆线程 - JDK 5:ExecutorService + JUC,并发工具库完善 - JDK 8:CompletableFuture + Stream,函数式并发 - JDK 21:Virtual Threads,轻量级并发新时代

如果你正在开发IO密集型服务,强烈建议开始尝试虚拟线程。它不是银弹,但在合适的场景下,它能帮你用更少的资源、更简单的代码,实现更高的吞吐量。

下一篇文章,我将介绍JDK 21另一个重磅特性——结构化并发(Structured Concurrency),它与虚拟线程是最佳拍档,敬请期待!

---

参考资料: - [JEP 444: Virtual Threads](https://openjdk.org/jeps/444) - [Java 21 Documentation](https://docs.oracle.com/en/java/javase/21/) - Spring Boot 3.2 Release Notes

http://www.jsqmd.com/news/684968/

相关文章:

  • CatBoost在房价预测中的优势与实践指南
  • MATLAB小波分析保姆级教程:从数据导入到实部等值线图,手把手搞定周期性分析
  • 图像增强技术解析:从基础几何变换到高级GAN应用
  • 解码胰岛素信号网络:从分子蓝图到代谢重塑
  • Git冲突实战:当IDEA/VSCode图形化界面失灵时,如何用纯命令‘救场’?
  • 2026防护钢板网技术全解析:四川菱形防护网,四川金属板网,四川钢板拉伸网,四川钢板网,四川防护网,优选指南! - 优质品牌商家
  • Unity新手避坑指南:从Asset Store到项目,DoTween插件安装配置全流程(含ASMDEF文件生成)
  • TTS-Backup:3分钟学会保护你的桌游模拟器珍贵存档
  • Python数据清洗实战:机器学习预处理关键技术
  • IAR Embedded Workbench 保姆级配置指南:从字体配色到终端打印,打造你的专属开发环境
  • 2026年比较好的红油豆瓣/郫县豆瓣公司哪家好 - 品牌宣传支持者
  • 给你的ESP32桌面时钟“连上网”:用MicroPython+ST7735屏实现NTP自动校时
  • 实战指南:MyBatisPlus核心查询方法selectById、selectOne、selectBatchIds、selectByMap、selectPage的典型业务场景解析
  • p75 NGF受体重组兔单抗能否示踪骨骼修复的细胞迁移?
  • 数据库事务隔离级别:可重复读与幻读的解决方案对比
  • 怎样全面评估智慧校园平台的性价比?这几点值得参考
  • RV1126嵌入式QT应用实战:从Buildroot集成到屏幕点亮
  • Playwright实战-在gitlab ci环境运行自动化测试
  • Android 开发警告信息:Static member ‘FaceIdentifyManager.init(...)‘ accessed via instance reference
  • 3步解锁!用TranslucentTB打造你的专属Windows透明任务栏
  • 置信区间在房地产数据分析中的Python实现与应用
  • 后量子密码中的拒绝采样技术及硬件优化
  • golang如何设计RESTful API命名规范_golang RESTful API命名规范思路
  • PDF转长图终极指南:三种方法,轻松将多页文档变为一张吸睛长图
  • 【紧急避坑】GraalVM静态镜像启动即崩?92%开发者忽略的--initialize-at-build-time误用与3种安全初始化策略
  • Blazor开发人力成本飙升真相,深度拆解:为什么团队在.NET 9+中多花37%工时?——附自动化诊断工具包下载
  • 保姆级教程:用K210和STM32F103玩转串口通信(附完整代码与接线图)
  • CSS如何实现文本溢出显示省略号_掌握text-overflow使用方法
  • 任务分解到可执行 Action:从自然语言到 Action Schema 的转换流程
  • 学工平台让学生请假告别繁琐,移动审批随时处理