17- [并发与调度] 异步任务和虚拟线程

5. [并发与调度] 异步任务和虚拟线程

摘要: 并非所有任务都需要在一次 HTTP 请求的生命周期内同步完成。本章我们将深入 Spring Boot 的并发编程模型,学习如何将耗时操作(如发送邮件、生成报表)异步化以提升用户体验。我们将掌握传统的 @Async 异步任务和 @Scheduled 定时任务,并重点探索 Spring Boot 3.2+ 对 JDK 21+ 虚拟线程 (Virtual Threads) 的革命性支持,体验新一代高并发编程的魅力。

开始本节前的准备工作:清理认证代码

为了让我们的学习焦点更集中,我们将暂时移除之前添加的 Token 认证逻辑。请按照以下清单,删除对应的文件和代码:

1. 删除以下文件:

  • 文件路径: demo-framework/src/main/java/com/example/demoframework/interceptor/AuthInterceptor.java
  • 文件路径: demo-framework/src/main/java/com/example/demoframework/config/SpringDocConfig.java
  • 文件路径: demo-system/src/main/java/com/example/demosystem/controller/AuthController.java
  • 文件路径: demo-system/src/main/java/com/example/demosystem/dto/auth/LoginDTO.java

2. 修改 WebConfig.java:

打开 WebConfig.java 文件,删除注释掉注册 AuthInterceptor 的相关代码。

文件路径: demo-framework/src/main/java/com/example/demoframework/config/WebConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.example.demoframework.config;

// 移除 import com.example.demoframework.interceptor.AuthInterceptor;
import com.example.demoframework.interceptor.LogInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

private final LogInterceptor logInterceptor;
// 移除 private final AuthInterceptor authInterceptor;

// ... 已有的其他 Bean 和方法 ...

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logInterceptor)
.addPathPatterns("/**");

/*
* ↓↓↓ 删除下面这段注册 AuthInterceptor 的代码 ↓↓↓
*/
// registry.addInterceptor(authInterceptor)
// .addPathPatterns("/**")
// .excludePathPatterns(
// "/auth/login",
// "/files/**",
// "/springboot-uploads/**",
// "/swagger-ui/**",
// "/v3/api-docs/**"
// );
}
}

完成以上清理工作后,请重新加载 Maven 项目,确保项目可以正常编译和启动。

5.1. 经典异步编程:@Async 与自定义线程池

5.1.1. 痛点分析与异步启用

场景故事 (痛点)

想象一下我们 UserService 中的 saveUser 方法。在成功创建用户后,产品经理提出了一个新需求:需要给新用户发送一封欢迎邮件。如果我们直接在 saveUser 方法里加入邮件发送逻辑,而这个邮件服务可能因为网络等原因需要耗时3秒,那么用户在前端点击“注册”按钮后,必须在页面上“转圈”等待3秒以上,才能收到“注册成功”的提示。这是一种糟糕的用户体验。

对于这类非核心、可延迟处理的耗时任务,最佳解决方案就是将其异步化:主线程(处理 HTTP 请求的线程)在触发邮件发送任务后,不等待其完成,而是立即返回响应,将真正的发送操作交给后台的另一个线程去处理。@Async 正是 Spring 为此提供的优雅实现。

第一步:启用异步 (@EnableAsync)

首先,我们需要在项目中显式地开启对异步任务的支持。最佳实践是在 demo-framework 模块中创建一个专门的配置类。

文件路径: demo-framework/src/main/java/com/example/demoframework/config/AsyncConfig.java (新增)

1
2
3
4
5
6
7
8
9
package com.example.demoframework.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;

@Configuration
@EnableAsync // 关键注解:全局开启 Spring 的异步方法执行功能
public class AsyncConfig {
}

5.1.2. 实战:创建并调用异步方法

第二步:创建异步服务

我们在 demo-system 模块中创建一个专门处理通知的服务,并在其中定义一个异步方法。

文件路径: demo-system/src/main/java/com/example/demosystem/service/NotificationService.java (新增)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.example.demosystem.service;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class NotificationService {

/**
* @Async 注解表明这是一个异步方法。
* Spring 会在调用此方法时,从一个线程池中获取一个线程来执行它,
* 而不会阻塞原始的调用方线程。
*/
@Async
@SneakyThrows // Lombok注解,用于优雅地处理受检异常(这里是InterruptedException)
public void sendWelcomeEmailAsync(String username) {
log.info("[TID:{}] [Async] 开始为用户 '{}' 发送欢迎邮件...", Thread.currentThread().getId(), username);
// 模拟耗时的邮件发送过程
Thread.sleep(3000);
log.info("[TID:{}] [Async] 欢迎邮件发送成功, 用户: '{}'", Thread.currentThread().getId(), username);
}
}

第三步:集成到业务流程

现在,我们在 UserServiceImpl 中调用这个新的异步方法。

文件路径: demo-system/src/main/java/com/example/demosystem/service/impl/UserServiceImpl.java (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ... imports ...
import com.example.demosystem.service.NotificationService; // 导入新服务

@Service
@RequiredArgsConstructor
@Slf4j
public class UserServiceImpl implements UserService {

private final UserMapper userMapper;
private final NotificationService notificationService; // 通过构造函数注入

@Override
@Transactional(rollbackFor = Exception.class)
public Long saveUser(UserEditDTO dto) {
// ... 已有的业务校验 ...
User user = Convert.convert(User.class, dto);
user.setStatus(1);
user.setCreateTime(LocalDateTime.now());
userMapper.insert(user);

// 触发异步邮件发送,主流程不等待
notificationService.sendWelcomeEmailAsync(dto.getUsername());

log.info("[TID:{}] 'saveUser' 方法执行完毕,即将返回响应", Thread.currentThread().getId());
return user.getId();
}

// ... 其他方法 ...
}

第四步:验证效果

  1. 重启 demo-admin 应用。
  2. 调用 POST /users 接口新增一个用户。
  3. 观察现象:
    • API 响应: 您会发现接口几乎是立即返回了 201 Created 的成功响应。
    • 控制台日志: 日志的打印顺序将完美地展示异步执行流程。
1
2
3
4
5
6
7
8
9
// Tomcat HTTP 线程(例如 TID:48)立即完成并返回响应
[TID:48] 'saveUser' 方法执行完毕,即将返回响应
// 后台线程池中的一个线程(例如 TID:77)开始执行耗时任务
[TID:77] [Async] 开始为用户 'newUserFromAsyncTest' 发送欢迎邮件...

--- (此处会暂停约 3 秒钟) ---

// 3秒后,后台线程完成任务
[TID:77] [Async] 欢迎邮件发送成功, 用户: 'newUserFromAsyncTest'

结论: @Async 成功地将耗时3秒的邮件发送任务从主请求流程中剥离,极大地优化了用户体验。


5.1.3. 生产级配置:自定义线程池

严重警告: Spring Boot 在默认情况下使用的 SimpleAsyncTaskExecutor 是一个极其危险的线程池。它不会复用线程,而是为每一个 @Async 调用都创建一个全新的线程。在高并发场景下,这将迅速耗尽服务器的内存和线程资源,导致应用崩溃。在生产环境中,必须自定义线程池!

我们在 AsyncConfig 中创建一个 ThreadPoolTaskExecutor Bean 来覆盖默认配置。

文件路径: demo-framework/src/main/java/com/example/demoframework/config/AsyncConfig.java (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.example.demoframework.config;

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;

@Configuration
@EnableAsync
public class AsyncConfig {

/**
* 自定义一个线程池 Bean。
* Spring 在查找 @Async 使用的线程池时,会默认寻找名为 "taskExecutor" 的 Bean。
* @return Executor
*/
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数:CPU核心数
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
// 最大线程数:CPU核心数 * 2
executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 2);
// 任务队列容量
executor.setQueueCapacity(256);
// 线程名称前缀,便于日志追踪
executor.setThreadNamePrefix("MyAsync-");
// 初始化线程池
executor.initialize();
return executor;
}
}

重启应用并再次测试,您会发现异步任务日志中的线程名已经变成了 MyAsync-1,证明我们的自定义线程池已成功生效。


5.1.4. [进阶] 处理异步方法的返回值

如果我们需要获取异步任务的执行结果怎么办?@Async 方法可以通过返回 java.util.concurrent.CompletableFuture; 的实现类来做到这一点。

实践步骤

  1. 修改 NotificationService:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.example.demosystem.service;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
@Slf4j
public class NotificationService {
/**
* 使用 CompletableFuture 作为异步方法的返回值。
* 这是目前Spring官方推荐的最佳实践。
* @param username 用户名
* @return 一个 CompletableFuture 对象,它将在任务完成时包含结果字符串
*/
@Async
@SneakyThrows
public CompletableFuture<String> sendWelcomeEmailAsync(String username) {
log.info("[TID:{}] [Async] 开始为用户 '{}' 发送欢迎邮件...", Thread.currentThread().getId(), username);

// 模拟耗时的邮件发送过程
Thread.sleep(3000);

String result = "欢迎邮件已成功发送给 " + username;
log.info("[TID:{}] [Async] 欢迎邮件发送成功, 用户: '{}'", Thread.currentThread().getId(), username);

// 使用 CompletableFuture.completedFuture() 创建一个已完成的 CompletableFuture
// 这等同于之前 new AsyncResult<>(result) 的作用
return CompletableFuture.completedFuture(result);
}
}

5.1.4. [进阶] 处理异步方法的返回值

如果我们需要获取异步任务的执行结果怎么办?@Async 方法可以通过返回 java.util.concurrent.Future 的实现类来做到这一点。

实践步骤

  1. 修改 NotificationService:
1
2
3
4
5
6
7
8
9
10
11
12
13
// NotificationService.java
import org.springframework.scheduling.annotation.AsyncResult; // Spring 提供的 Future 实现
import java.util.concurrent.Future;

// ...
@Async
@SneakyThrows
public Future<String> getAsyncTaskResult(String taskName) {
log.info("开始执行异步任务: {}", taskName);
Thread.sleep(2000);
log.info("异步任务 '{}' 执行完毕", taskName);
return new AsyncResult<>("Result of " + taskName);
}
  1. 创建测试 Controller:

文件路径: demo-system/src/main/java/com/example/demosystem/controller/AsyncTestController.java (新增)

重要信息: 注意,为了方便测试,我们在SpringMVC定义的Token验证的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.example.demosystem.controller;

import com.example.demosystem.service.NotificationService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.CompletableFuture; // 导入 CompletableFuture

@RestController
@RequiredArgsConstructor
@Slf4j
@Tag(name = "异步任务测试")
public class AsyncTestController {

private final NotificationService notificationService;

@GetMapping("/async-result")
@Operation(summary = "测试异步任务结果(非阻塞方式)")
public CompletableFuture<String> testAsyncResult() {
log.info("[Request Thread TID:{}] Controller 开始调用异步方法...", Thread.currentThread().getId());

CompletableFuture<String> futureResult = notificationService.getAsyncTaskResult("Task1");

log.info("[Request Thread TID:{}] Controller 调用异步方法完毕,立即返回CompletableFuture,请求线程被释放。", Thread.currentThread().getId());

// 直接返回 CompletableFuture 对象
// Spring MVC会接管它,等待它完成,然后将结果写入响应
// 我们可以链式地添加一些处理逻辑
return futureResult.thenApply(result -> {
// 这个 .thenApply 里的代码会在异步任务完成时执行,通常在另一个线程中
log.info("[Callback Thread TID:{}] 异步任务成功,准备返回结果: {}", Thread.currentThread().getId(), result);
return "成功获取到异步任务的结果: " + result;
});
}
}

验证效果: 调用 GET /async-result 接口。您会观察到,浏览器会等待约2秒后才收到响应。控制台日志会显示,Controller 先打印了“调用完毕”,然后才在调用 thenApply() 后打印“成功获取结果”,这清晰地展示了主线程被阻塞以等待异步结果的过程。


5.2. 定时任务调度:@Scheduled

场景故事 (痛点)

随着应用的运行,数据库中的操作日志表越来越大,影响查询性能。运维团队希望我们能开发一个功能,在每天流量最低的凌晨3点,自动将30天前的旧日志数据迁移到归档表中。这就需要一个无需人工干预、能按预定时间自动触发的机制。@Scheduled 注解正是 Spring 提供的解决此类需求的标准方案。

5.2.1. 核心语法与启用

在动手之前,我们首先需要掌握 @Scheduled 注解的几种核心调度策略。

属性核心作用计时基准
fixedRate固定速率执行从上一次任务的开始时间计算
fixedDelay固定延迟执行从上一次任务的结束时间计算
cronCron 表达式指定的时间点执行由表达式定义(如每天凌晨3点)

启用调度 (@EnableScheduling)

@EnableAsync 类似,我们需要先在项目中开启对定时任务的支持。

文件路径: demo-admin/src/main/java/com/example/demoadmin/SpringBootDemoApplication.java (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.demoadmin;

// ... imports ...
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication(scanBasePackages = "com.example")
@MapperScan("com.example.*.mapper")
@EnableCaching
@EnableRetry
@EnableScheduling // 启用 Spring 定时任务功能
public class SpringBootDemoApplication {
// ... main method ...
}

5.2.2. 实战:创建定时任务

现在,我们在 demo-system 模块中创建一个专门存放定时任务的组件,并一次性实现多种调度策略。

文件路径: demo-system/src/main/java/com/example/demosystem/tasks/SystemTasks.java (新增)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.example.demosystem.tasks;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Component
@Slf4j
public class SystemTasks {

private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");

/**
* fixedRate: 固定速率执行。
* 每隔 5 秒执行一次。计时是从任务的【开始时间】算起的。
*/
@Scheduled(fixedRate = 5000)
@SneakyThrows
public void runAtFixedRate() {
log.info("FixedRate Task - 开始 @ {}", formatter.format(LocalDateTime.now()));
Thread.sleep(2000); // 模拟耗时2秒
log.info("FixedRate Task - 结束 @ {}", formatter.format(LocalDateTime.now()));
}

/**
* fixedDelay: 固定延迟执行。
* 在上一次任务【执行完毕】后,延迟 3 秒再执行下一次。
*/
@Scheduled(fixedDelay = 3000)
@SneakyThrows
public void runAtFixedDelay() {
log.info("FixedDelay Task - 开始 @ {}", formatter.format(LocalDateTime.now()));
Thread.sleep(2000); // 模拟耗时2秒
log.info("FixedDelay Task - 结束 @ {}", formatter.format(LocalDateTime.now()));
}

/**
* cron 表达式,用于在特定时间点执行任务。
* 这个表达式表示 "在每天的凌晨 3 点 0 分 0 秒执行"。
* 我不建议去记忆Cron表达式的语法,使用场景单一,需要的时候用AI生成一个即可
*/
@Scheduled(cron = "0 0 3 * * ?")
public void archiveOldLogs() {
log.info("CRON Task - 开始执行日志归档任务 @ {}", LocalDateTime.now());
// ... 在此执行真实的日志归档数据库操作 ...
}
}

5.2.3. 验证与对比

重启应用并观察控制台日志,您会清晰地看到 fixedRatefixedDelay 的区别。

fixedRate (固定速率) 验证

fixedRate 强调的是按固定频率触发。它以任务的上一次开始执行的时间点为基准,来计算下一次的开始时间。

1
2
3
4
5
6
7
// 以下任务代码,其执行日志如下所示
@Scheduled(fixedRate = 5000)
public void runAtFixedRate() {
log.info("FixedRate Task - 开始 @ {}", ...);
Thread.sleep(2000);
log.info("FixedRate Task - 结束 @ {}", ...);
}
1
2
3
4
5
6
FixedRate Task - 开始 @ 11:56:02
FixedRate Task - 结束 @ 11:56:04
FixedRate Task - 开始 @ 11:56:07 <-- 距离上一次【开始】正好 5 秒
FixedRate Task - 结束 @ 11:56:09
FixedRate Task - 开始 @ 11:56:12 <-- 距离上一次【开始】正好 5 秒
FixedRate Task - 结束 @ 11:56:14

fixedDelay (固定延迟) 验证

fixedDelay 强调的是在上一次任务结束后,再等待一个固定的延迟时间。

1
2
3
4
5
6
7
// 以下任务代码,其执行日志如下所示
@Scheduled(fixedDelay = 3000)
public void runAtFixedDelay() {
log.info("FixedDelay Task - 开始 @ {}", ...);
Thread.sleep(2000);
log.info("FixedDelay Task - 结束 @ {}", ...);
}
1
2
3
4
5
6
FixedDelay Task - 开始 @ 11:56:02
FixedDelay Task - 结束 @ 11:56:04
FixedDelay Task - 开始 @ 11:56:07 <-- 距离上一次【结束】正好 3 秒
FixedDelay Task - 结束 @ 11:56:09
FixedDelay Task - 开始 @ 11:56:12 <-- 距离上一次【结束】正好 3 秒
FixedDelay Task - 结束 @ 11:56:14

总结: fixedRate 适合对执行频率要求严格的任务(如每分钟的心跳检测),而 fixedDelay 适合不希望任务并发执行、需要保证执行间隔的场景(如轮询数据库)。


5.3. [前沿] JDK 21+ 虚拟线程:新时代的并发模型

5.3.1. 痛点:传统平台线程的瓶颈分析

5.1 节,我们配置了自定义线程池,但其底层使用的仍是传统的 平台线程。平台线程是 Java 线程的经典实现,其核心特征是与操作系统内核线程存在一对一的直接映射关系。

这种 1:1 的映射模型带来了两个固有的限制:

  1. 资源成本高: 每个平台线程都对应一个操作系统内核线程。创建和管理内核线程对操作系统而言是高成本操作,需要分配独立的栈内存并涉及昂贵的上下文切换。
  2. 数量有限: 由于其资源消耗,一台服务器能同时有效运行的平台线程数量通常被限制在几百到几千的规模。

在 Spring Boot 的传统阻塞式 Web 模型中,每个 HTTP 请求在处理期间都会独占一个平台线程。如果该请求中包含一个耗时 200ms 的数据库查询,那么在这 200ms 的 I/O 等待期间,这个平台线程会进入阻塞 (Blocking) 状态。尽管此时 CPU 可能处于空闲,但该线程资源被完全占用,无法执行任何其他任务。

核心瓶颈

当大量并发请求涌入,特别是当这些请求大多是 I/O 密集型(如等待数据库、外部 API 响应)时,有限的平台线程池会迅速被占满。后续的请求不得不进入队列等待,导致应用程序的吞吐量(TPS)达到上限,无法进一步扩展。


解决方案:虚拟线程

虚拟线程 是 JDK 引入的一种轻量级线程实现,由 JVM 直接调度和管理,而非操作系统。

它与平台线程的核心区别在于,虚拟线程与操作系统内核线程之间不再是 1:1 的映射关系。相反,大量的虚拟线程可以运行在一个由少量平台线程构成的池(这些平台线程被称为载体线程, Carrier Threads)之上。

虚拟线程的革命性机制在于其处理阻塞 I/O 的方式:

当一个虚拟线程执行一个阻塞操作时,JVM 会自动将其挂起,并从其载体平台线程上**“卸载” (unmount)**。这使得该平台线程可以立即被释放,去执行其他准备就绪的虚拟线程。当 I/O 操作完成后,JVM 会将原来的虚拟线程重新调度到任意一个可用的载体线程上继续执行。

这种机制带来了显著优势:

  1. 创建成本极低: 虚拟线程是 JVM 管理的轻量级实体,本质上是 Java 堆上的对象,不直接消耗宝贵的操作系统线程资源。
  2. 支持海量并发: 由于成本低廉,单个 JVM 实例可以轻松创建和管理数百万个虚拟线程。

通过这种高效的协作和调度机制,极少数的平台线程便能支撑起海量虚拟线程的并发执行。对于 I/O 密集型应用而言,这意味着线程不再是瓶颈,应用的吞吐能力和资源利用率得到极大提升。


5.3.2. [实践] 一键开启:在 Spring Boot 中启用虚拟线程

Spring Boot 3.2+ 对 JDK 21+ 的虚拟线程提供了无与伦比的、一等公民级的支持。开启它,简单到令人难以置信。
前置条件:

  1. 确保您的项目使用 JDK 21 或更高版本
  2. 确保您的 Spring Boot 版本为 3.2 或更高

由于我们的设置版本是17,这里就不测试了,核心也只是一个配置项而已

1. 修改配置文件

我们只需在 application.yml 中添加一行配置。

文件路径: demo-admin/src/main/resources/application.yml (修改)

1
2
3
4
5
spring:
# ... 其他配置 ...
threads:
virtual:
enabled: true # 开启虚拟线程

就这么简单! 加上这行配置后,Spring Boot 会自动将内部的 Tomcat Web 服务器切换到使用虚拟线程来处理每一个进来的 HTTP 请求。


5.3.3. 性能对比:虚拟线程 vs. 传统线程池的适用场景

既然虚拟线程如此强大且易于开启,是否意味着我们之前配置的 @Async 传统线程池就过时了呢?答案是:并非如此。它们是为解决不同问题而设计的。

对比维度平台线程 (我们自定义的线程池)虚拟线程 (Spring Boot 自动管理)
核心本质珍贵的操作系统内核线程的直接映射JVM 管理的、轻量级的用户态线程
资源成本 (创建和上下文切换开销大)极低 (几乎没有额外开销)
最佳场景CPU 密集型任务 (如:复杂计算、图像处理、数据加密)I/O 密集型任务 (如:等待数据库、调用外部API、读写文件)
数量有限 (通常几十到几百)海量 (可轻松达到数百万)
使用方式通过 @Async 注解,用于后台异步计算通过配置开启,用于处理海量并发的 Web 请求
面试官深度辨析
今天 下午 3:00

既然 Spring Boot 3.2+ 开启虚拟线程如此简单,那我们为 @Async 自定义的 ThreadPoolTaskExecutor 线程池还有存在的必要吗?

求职者

非常有必要!它们解决的是不同维度的问题。

求职者

虚拟线程 的核心优势是解决海量并发 I/O 密集型 任务的等待问题,比如成千上万的用户同时请求我们的 API。它的设计目标是“不阻塞平台线程”,从而提高系统吞吐量。

求职者

而我们为 @Async 自定义的 平台线程池,其核心优势是处理后台的 CPU 密集型 任务。这类任务需要持续占用 CPU 进行计算,我们通过一个固定大小(比如等于 CPU 核心数)的线程池来执行它们,可以避免频繁的线程创建和销毁,并防止过多的线程因争抢 CPU 而导致性能下降。

明白了。所以可以总结为:用虚拟线程处理前端进来的海量 I/O 请求,用平台线程池处理后台的重量级计算任务。

求职者

完全正确。这正是 Spring Boot 3.2+ 所倡导的现代并发编程模型,两者相得益彰,而非互相替代。

总结: 虚拟线程是 Java 和 Spring Boot 在高并发领域的一项革命性进步。它以极低的成本,极大地提升了传统 I/O 密集型应用的性能和可伸缩性。在您的下一个项目中,如果使用了 JDK 21+ 和 Spring Boot 3.2+,请毫不犹豫地开启它。