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

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 丢失

问题:异步线程中拿不到当前用户、请求头等信息。

原因SecurityContextRequestContextHolder 默认使用 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 或性能下降

配置项建议值说明
corePoolSize10-20核心线程数
maxPoolSize50-100最大线程数
queueCapacity100-1000队列大小,避免无限堆积
keepAliveSeconds60空闲线程存活时间
rejectedExecutionHandlerCallerRunsPolicy拒绝策略,避免丢弃任务

✅ 生产环境建议结合监控动态调整。


🧩 五、高级用法: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成员变量可能被并发修改
@RepositoryDAO 通常只操作数据库,无状态
工具类(无状态)StringUtils
ThreadLocal每个线程独享

✅ 建议:

  • 避免在 Service 中定义可变成员变量
  • 使用 synchronizedReentrantLockConcurrentHashMap 保护共享资源

✅ 七、最佳实践总结

项目推荐做法
线程池自定义 ThreadPoolTaskExecutor,不使用 Executors
异步注解使用 @Async + 自定义线程池
上下文传递手动传递 SecurityContextRequestContextHolder
事务异步方法独立事务,避免依赖主事务
异常处理@Async 方法返回 CompletableFuture,便于捕获异常
监控暴露线程池指标(如 ThreadPoolTaskExecutorgetActiveCount()
拒绝策略使用 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
http://www.dtcms.com/a/351796.html

相关文章:

  • 问:单证硕士含金量是否不足?
  • 【Linux 进程】进程程序替换
  • 【GitHub】使用SSH与GitHub交互
  • 工业大模型五层架构全景解析:从算力底座到场景落地的完整链路
  • PyCharm注释详解:TODO、文档注释、注释
  • MySQL 索引:结构、对比与操作实践指南
  • 【合适新人】预测图片教程——如何随机抽取验证集图片进行可视化推理!(附完整代码)
  • DigitalOcean GPU 选型指南(三):中端AI GPU性价比之王 RTX 4000 Ada、A4000、A5000
  • 无人机航拍数据集|第33期 无人机树冠目标检测YOLO数据集5842张yolov11/yolov8/yolov5可训练
  • 【HZ-T536开发板免费体验】无需死记 Linux 命令!用 CangjieMagic 在 HZ-T536 开发板上搭建 MCP 服务器,自然语言轻松控板
  • Java大厂面试全真模拟:从Spring Boot到微服务架构实战
  • 文本转语音TTS工具合集(下)
  • 【强化学习】区分理解: 时序差分(TD)、蒙特卡洛(MC)、动态规划(DP)
  • 计算机底层硬件实现及运行原理通俗书籍推荐
  • 记一次MySQL数据库的操作练习
  • 把 AI 塞进「空调遥控器」——基于 MEMS 温湿阵列的 1 分钟极速房间热场扫描
  • 如何获取当前页面html元素的外层容器元素
  • vscode或者cursor配置使用Prettier - Code formatter来格式化微信小程序wxss/wxs/wxml文件
  • Vue Flow 设计大模型工作流 - 自定义大模型节点
  • 基于XiaothinkT6语言模型的文本相似度计算:轻量方案实现文本匹配与去重
  • 乳腺癌数据集支持向量机实践学习总结
  • 2025最新的软件测试热点面试题(答案+解析)
  • OnlyOffice 渲染时间获取指南
  • from中烟科技翼支付 面试题2
  • 项目集升级:顶部导览优化、字段自定义、路线图双模式、阶段图掌控、甘特图升级、工作量优化、仪表盘权限清晰
  • 用大语言模型提升语音翻译:一种全新的端到端方法
  • vue2+elementui 表格单元格增加背景色,根据每列数据的大小 颜色依次变浅显示2
  • 「大模型学习」(15)Prompt Tuning → P-Tuning v1 → P-Tuning v2
  • (论文速读)Prompt Depth Anything:让深度估计进入“提示时代“
  • 6.5 el-tree 组件