springweb项目中多线程使用详解
🧱 一、为什么在 Spring Web 中要用多线程?
场景 | 说明 |
---|---|
异步处理 | 发送邮件、短信、日志记录等耗时操作不阻塞主流程 |
并行计算 | 一个请求需要调用多个远程服务,可并行执行 |
定时任务 | @Scheduled 自动在后台线程执行 |
提高吞吐量 | 避免主线程阻塞,提升 QPS |
🛠 二、Spring 中多线程的核心机制
1. @Async
注解 + 异步方法
- Spring 提供的最简单异步方式
- 方法上加
@Async
,调用时自动提交到线程池执行
2. 线程池(ThreadPoolTaskExecutor)
- Spring 推荐使用
ThreadPoolTaskExecutor
而不是Executors.newXXX()
- 可配置核心线程数、队列、拒绝策略等
3. CompletableFuture
- Java 8+ 提供的异步编程工具
- 支持链式调用、组合多个异步任务
4. @Scheduled
定时任务
- 自动在后台线程执行定时任务
🚀 三、实战:配置自定义线程池(推荐做法)
✅ 步骤 1:配置线程池 Bean
@Configuration
@EnableAsync
@EnableScheduling
public class AsyncConfig {@Value("${async.core-pool-size:10}")private int corePoolSize;@Value("${async.max-pool-size:50}")private int maxPoolSize;@Value("${async.queue-capacity:100}")private int queueCapacity;@Bean("taskExecutor")public Executor taskExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(corePoolSize);executor.setMaxPoolSize(maxPoolSize);executor.setQueueCapacity(queueCapacity);executor.setThreadNamePrefix("async-task-");executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 拒绝策略executor.initialize();return executor;}
}
✅ 注意:
@EnableAsync
:启用异步支持@EnableScheduling
:启用定时任务CallerRunsPolicy
:当线程池满时,由调用线程执行任务(防止丢弃)
✅ 步骤 2:使用 @Async
注解
@Service
public class AsyncService {@Async("taskExecutor") // 指定使用哪个线程池public CompletableFuture<String> sendEmail(String to, String content) {try {Thread.sleep(2000); // 模拟发送邮件System.out.println("邮件已发送到: " + to + ",线程: " + Thread.currentThread().getName());return CompletableFuture.completedFuture("邮件发送成功");} catch (Exception e) {return CompletableFuture.failedFuture(e);}}@Async("taskExecutor")public void logOperation(String operation) {System.out.println("记录日志: " + operation + ",线程: " + Thread.currentThread().getName());// 写入数据库或文件}
}
✅ 步骤 3:在 Controller 中调用异步方法
@RestController
public class UserController {@Autowiredprivate AsyncService asyncService;@GetMapping("/user/{id}")public String getUser(@PathVariable String id) throws ExecutionException, InterruptedException {System.out.println("主线程: " + Thread.currentThread().getName());// 1. 查询用户(同步)String user = "User-" + id;// 2. 异步发送邮件CompletableFuture<String> emailFuture = asyncService.sendEmail("user@example.com", "欢迎注册");// 3. 异步记录日志asyncService.logOperation("查询用户 " + id);// 4. 等待异步结果(可选)String emailResult = emailFuture.get(); // 阻塞等待return user + ",邮件结果: " + emailResult;}
}
✅ 输出示例:
主线程: http-nio-8080-exec-1
邮件已发送到: user@example.com,线程: async-task-1
记录日志: 查询用户 1,线程: async-task-2
⚠️ 四、常见问题与解决方案
❌ 问题 1:@Async
不生效(最常见)
原因:
- 方法在同一个类中被内部调用,不经过代理
- 没有加
@EnableAsync
- 方法不是
public
✅ 解决方案:
@Service
public class UserService {@Autowiredprivate UserService self; // 自注入public void register() {// 调用代理对象的方法self.sendWelcomeEmail();}@Asyncpublic void sendWelcomeEmail() {// ...}
}
❌ 问题 2:SecurityContext / RequestContextHolder 丢失
问题:异步线程中拿不到当前用户、请求头等信息。
原因:SecurityContext
和 RequestContextHolder
默认使用 ThreadLocal
,子线程无法继承。
✅ 解决方案:手动传递上下文
@Async("taskExecutor")
public void processWithSecurity() {// 1. 获取主线程的 SecurityContextSecurityContext context = SecurityContextHolder.getContext();// 2. 在异步线程中设置SecurityContextHolder.setContext(context);try {// 执行业务逻辑System.out.println("当前用户: " + context.getAuthentication().getName());} finally {SecurityContextHolder.clearContext();}
}
✅ 推荐封装成工具类或使用
InheritableThreadLocal
。
❌ 问题 3:事务失效
问题:@Transactional
方法中调用 @Async
方法,异步方法不在同一个事务中。
原因:异步方法在另一个线程执行,事务是线程绑定的。
✅ 解决方案:
- 异步方法本身开启新事务(
@Transactional(propagation = Propagation.REQUIRES_NEW)
) - 或者:异步方法不依赖事务,先提交主事务,再异步处理
@Async("taskExecutor")
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void asyncSaveLog() {// 新事务
}
❌ 问题 4:线程池配置不合理导致 OOM 或性能下降
配置项 | 建议值 | 说明 |
---|---|---|
corePoolSize | 10-20 | 核心线程数 |
maxPoolSize | 50-100 | 最大线程数 |
queueCapacity | 100-1000 | 队列大小,避免无限堆积 |
keepAliveSeconds | 60 | 空闲线程存活时间 |
rejectedExecutionHandler | CallerRunsPolicy | 拒绝策略,避免丢弃任务 |
✅ 生产环境建议结合监控动态调整。
🧩 五、高级用法:CompletableFuture
实现并行调用
@GetMapping("/user/detail/{id}")
public CompletableFuture<UserDetail> getUserDetail(@PathVariable String id) {return CompletableFuture.allOf(asyncService.fetchUserInfo(id),asyncService.fetchUserOrders(id),asyncService.fetchUserProfile(id)).thenApply(v -> {// 所有任务完成后组合结果return buildUserDetail(id);});
}
✅ 优势:多个远程调用并行执行,总耗时 = 最慢的那个。
📊 六、线程安全问题
Spring Bean 默认是单例,在多线程环境下要注意:
类型 | 是否线程安全 | 说明 |
---|---|---|
@Controller , @Service | ❌ | 成员变量可能被并发修改 |
@Repository | ✅ | DAO 通常只操作数据库,无状态 |
工具类(无状态) | ✅ | 如 StringUtils |
ThreadLocal | ✅ | 每个线程独享 |
✅ 建议:
- 避免在 Service 中定义可变成员变量
- 使用
synchronized
、ReentrantLock
或ConcurrentHashMap
保护共享资源
✅ 七、最佳实践总结
项目 | 推荐做法 |
---|---|
线程池 | 自定义 ThreadPoolTaskExecutor ,不使用 Executors |
异步注解 | 使用 @Async + 自定义线程池 |
上下文传递 | 手动传递 SecurityContext 、RequestContextHolder |
事务 | 异步方法独立事务,避免依赖主事务 |
异常处理 | @Async 方法返回 CompletableFuture ,便于捕获异常 |
监控 | 暴露线程池指标(如 ThreadPoolTaskExecutor 的 getActiveCount() ) |
拒绝策略 | 使用 CallerRunsPolicy 防止任务丢失 |
🎯 八、推荐配置模板(application.yml)
async:core-pool-size: 10max-pool-size: 50queue-capacity: 200management:endpoints:web:exposure:include: health,info,metrics,threaddump
📌 九、总结
场景 | 推荐方案 |
---|---|
简单异步任务 | @Async + 自定义线程池 |
并行调用 | CompletableFuture |
定时任务 | @Scheduled |
高并发任务 | @Async + CompletableFuture + 线程池监控 |
🧩 十、功能 2:自定义异步上下文传递工具类
目标
解决 @Async
中:
SecurityContext
丢失TraceId
(如 Sleuth)丢失- 自定义
ThreadLocal
上下文丢失
✅ 步骤 1:定义上下文载体
// AsyncContext.java
public class AsyncContext {private final SecurityContext securityContext;private final Map<String, String> requestContext; // 如 TraceId, UserIdpublic AsyncContext() {this.securityContext = SecurityContextHolder.getContext();this.requestContext = new HashMap<>();// 例如:MDC 中的 TraceIdthis.requestContext.put("traceId", MDC.get("traceId"));this.requestContext.put("userId", getUserName());}private String getUserName() {Authentication auth = securityContext.getAuthentication();return auth != null ? auth.getName() : null;}// Getter 方法public SecurityContext getSecurityContext() { return securityContext; }public Map<String, String> getRequestContext() { return requestContext; }
}
✅ 步骤 2:创建上下文传递装饰器
// ContextCopyingDecorator.java
public class ContextCopyingDecorator implements Executor {private final Executor target;public ContextCopyingDecorator(Executor executor) {this.target = executor;}@Overridepublic void execute(Runnable command) {AsyncContext context = new AsyncContext();target.execute(() -> {try {// 恢复上下文if (context.getSecurityContext() != null) {SecurityContextHolder.setContext(context.getSecurityContext());}if (context.getRequestContext() != null) {context.getRequestContext().forEach(MDC::put);}// 执行原任务command.run();} finally {// 清理SecurityContextHolder.clearContext();context.getRequestContext().keySet().forEach(MDC::remove);}});}
}
✅ 步骤 3:将装饰器应用到线程池
// AsyncConfig.java(修改)
@Bean("taskExecutor")
public Executor taskExecutor() {MonitoredThreadPoolTaskExecutor executor = new MonitoredThreadPoolTaskExecutor(meterRegistry, "business-task");executor.setCorePoolSize(corePoolSize);executor.setMaxPoolSize(maxPoolSize);executor.setQueueCapacity(queueCapacity);executor.setThreadNamePrefix("async-task-");executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());executor.initialize();// ✅ 装饰:添加上下文传递能力return new ContextCopyingDecorator(executor);
}
✅ 步骤 4:使用(无需修改业务代码)
@Async("taskExecutor")
public void logWithSecurity() {// 现在可以直接获取用户信息Authentication auth = SecurityContextHolder.getContext().getAuthentication();String user = auth != null ? auth.getName() : "anonymous";System.out.println("当前用户: " + user + ",线程: " + Thread.currentThread().getName());// 也可以获取 TraceIdString traceId = MDC.get("traceId");System.out.println("TraceId: " + traceId);
}
✅ 输出:
当前用户: zhangsan,线程: async-task-1
TraceId: abc-123-xyz