欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 艺术 > Java 21 虚拟线程 vs. 传统线程:Spring Boot 实战指南

Java 21 虚拟线程 vs. 传统线程:Spring Boot 实战指南

2025/6/21 4:41:00 来源:https://blog.csdn.net/dandandeshangni/article/details/148701550  浏览:    关键词:Java 21 虚拟线程 vs. 传统线程:Spring Boot 实战指南

图片

Java 21 中引入的虚拟线程 (Virtual threads),代表了 Java 并发模型的一次重大进步。本指南将通过一个 Spring Boot 应用,演示传统线程与虚拟线程之间的实际差异,展示虚拟线程如何在不受物理线程限制的情况下,轻松处理成千上万的并发请求。

先决条件

  • • Java 21 或更高版本

  • • Maven 或 Gradle

  • • 对 Spring Boot 有基本了解

  • • 对并发编程有基本了解


核心概念

传统线程 (Traditional Threads / 平台线程)
  • • 每个线程都直接映射到一个操作系统 (OS) 线程

  • • 数量受限于可用的 CPU 核心数和操作系统限制。

  • • 内存开销高 (通常每个线程约 1MB)。

  • • 阻塞操作会占用并“卡住”整个线程,使其无法执行其他任务。

虚拟线程 (Virtual Threads)
  • • 由 JVM 管理的轻量级线程

  • • 数量不受限于 CPU 核心数,可以创建数百万个。

  • • 内存开销极小 (通常每个线程仅几 KB)。

  • • 能高效处理阻塞操作,线程在阻塞时不会占用 OS 线程,可以去执行其他任务。

  • • 非常适合 I/O 密集型应用。


项目设置

  1. 1. 创建一个使用 Java 21 的新 Spring Boot 项目。

  2. 2. 将以下依赖项添加到你的 pom.xml 文件中 (或者你也可以使用 Gradle):
    <dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency>
    </dependencies>

实现步骤

1. 线程配置 (ThreadConfig.java)

创建一个配置类来分别配置传统线程池和虚拟线程的执行器 (Executor)。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import java.util.concurrent.Executor;
import java.util.concurrent.Executors;@Configuration
@EnableAsync// 启用异步方法执行
publicclassThreadConfig {// 定义传统线程池执行器@Bean(name = "traditionalExecutor")public Executor traditionalExecutor() {ThreadPoolTaskExecutorexecutor=newThreadPoolTaskExecutor();executor.setCorePoolSize(10);  // 核心线程数:10executor.setMaxPoolSize(10);   // 最大线程数:10 (线程数量有限)executor.setQueueCapacity(100); // 等待队列容量:100executor.setThreadNamePrefix("traditional-"); // 线程名前缀executor.initialize();return executor;}// 定义虚拟线程执行器@Bean(name = "virtualExecutor")public Executor virtualExecutor() {// 每次任务都创建一个新的虚拟线程return Executors.newVirtualThreadPerTaskExecutor();}
}
2. 测试控制器 (TestController.java)

创建一个控制器,包含两个端点,分别用于演示两种线程类型的行为。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;@RestController
@RequestMapping("/api/test")
publicclassTestController {privatestaticfinalLoggerlogger= LoggerFactory.getLogger(TestController.class);// 使用传统线程池异步执行@Async("traditionalExecutor")@GetMapping("/traditional")public CompletableFuture<String> traditionalThread() {logger.info("开始传统线程请求。线程: {}", Thread.currentThread().getName());try {TimeUnit.SECONDS.sleep(1); // 模拟耗时的I/O操作} catch (InterruptedException e) {Thread.currentThread().interrupt();return CompletableFuture.completedFuture("任务被中断");}return CompletableFuture.completedFuture("传统线程任务完成。线程: " + Thread.currentThread().getName());}// 使用虚拟线程执行器异步执行@Async("virtualExecutor")@GetMapping("/virtual")public CompletableFuture<String> virtualThread() {// 注意:日志中打印 Thread.currentThread() 会显示虚拟线程的详细信息logger.info("开始虚拟线程请求。线程: {}", Thread.currentThread());try {TimeUnit.SECONDS.sleep(1); // 模拟耗时的I/O操作} catch (InterruptedException e) {Thread.currentThread().interrupt();return CompletableFuture.completedFuture("任务被中断");}return CompletableFuture.completedFuture("虚拟线程任务完成。线程: " + Thread.currentThread());}
}

测试实现

1. 启动应用程序
./mvnw spring-boot:run
2. 测试传统线程

使用下面的命令模拟 200 个并发请求。由于线程池限制,你应该会看到一些请求失败。

# 测试200个并发请求(应该会看到一些失败或拒绝)
seq 1 200 | xargs -n1 -P200 curl -s "http://localhost:8080/api/test/traditional"
  • • 对上述命令的解释:

    • • seq 1 200: 生成从 1 到 200 的数字序列。

    • • |: 管道符,将前一个命令的输出作为后一个命令的输入。

    • • xargs: 将输入转换为命令行参数。

    • • -n1: 一次处理一个输入项。

    • • -P200: 最多并行运行 200 个进程。

    • • curl: 发起 HTTP 请求的命令。

    • • -s: 静默模式(不显示进度条或错误信息)。

    • • "http://.../traditional": 要测试的目标 URL。

3. 测试虚拟线程

使用下面的命令模拟 5000 个并发请求。所有请求应该都会成功。

# 测试5000个并发请求(应该都能成功)
seq 1 5000 | xargs -n1 -P5000 curl -s "http://localhost:8080/api/test/virtual"

预期结果

  • • 传统线程

    • • 最多同时处理 10 个并发请求,外加 100 个在队列中等待的请求。

    • • 超出 110 个的并发请求将会失败或超时。

    • • 日志中看到的线程名将是 "traditional-1" 到 "traditional-10"。

    • • 在大量并发请求下,内存使用率会显著增高。

  • • 虚拟线程

    • • 可以轻松处理数千个并发请求。

    • • 没有失败或超时

    • • 日志中看到的线程信息将类似于 VirtualThread[#ID]/runnable@ForkJoinPool-1-worker-1

    • • 内存开销极小。


理解结果

  • • 为什么传统线程会失败?

    • • 每个传统线程都消耗大量内存。

    • • 受到操作系统线程数量的限制。

    • • 阻塞操作会“霸占”整个线程,使其无法处理其他任务。

    • • 等待队列的容量也限制了能处理的并发请求总数。

  • • 为什么虚拟线程能成功?

    • • 它是 JVM 管理的轻量级实现。

    • • 能高效地处理阻塞操作:当虚拟线程遇到阻塞(如 sleep 或网络 I/O)时,JVM 会自动挂起它,并让底层的 OS 线程去执行其他任务,而不是空等。

    • • 几乎不受物理资源的限制,可以大量创建。

    • • 非常适合 I/O 密集型应用。


最佳实践

  • • 建议使用虚拟线程的场景:

    • • I/O 密集型应用 (例如,调用外部 API、数据库查询、文件读写)。

    • • 需要处理高并发请求的场景。

    • • 微服务架构中,需要同时处理大量并发请求的服务。

    • • 应用中包含大量阻塞操作的。

  • • 建议使用传统线程的场景:

    • • CPU 密集型任务 (例如,复杂的数学计算、数据加密/解密)。因为虚拟线程并不会提高 CPU 密集型任务的性能,这类任务需要与 CPU 核心数匹配的线程数才能最高效。

    • • 需要使用线程本地存储 (ThreadLocal) 并且依赖其与平台线程绑定的特性的场景 (虚拟线程对 ThreadLocal 的支持有限,且可能在不同 OS 线程上恢复执行)。

    • • 与不支持虚拟线程的遗留代码或原生库 (JNI) 交互时。


常见陷阱 (Common Pitfalls)

  • • 未使用 Java 21 或更高版本:虚拟线程是 Java 21 的正式功能。

  • • 在不理解其影响的情况下混合使用虚拟线程和传统线程:例如,在虚拟线程中调用一个 synchronized 同步块可能会“钉住 (pin)”虚拟线程,使其无法被挂起,从而降低性能优势。

  • • 未正确处理线程中断:无论是哪种线程,都应妥善处理 InterruptedException

  • • 未监控线程使用情况和性能:应使用适当的工具来监控应用的线程行为,以确保其按预期工作。


结论

虚拟线程是 Java 并发模型的一次重大进步,尤其对于 I/O 密集型应用而言。通过理解传统线程与虚拟线程之间的差异,开发者可以在构建应用程序时,做出更明智的技术选型决策。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词