目录
- 前言
- [一、@Async 的作用](#一、@Async 的作用)
- [二、@Async 底层是怎么工作的?](#二、@Async 底层是怎么工作的?)
- [三、只使用 @Async 会有什么问题?](#三、只使用 @Async 会有什么问题?)
- 四、为什么要自定义线程池?
- 五、自定义线程池示例
- [六、@Async("alertThreadPool") 是怎么找到线程池的?](#六、@Async("alertThreadPool") 是怎么找到线程池的?)
- 七、实际项目案例
- 八、线程名称的重要性
- 九、生产环境注意事项
- 十、总结
前言
在 Spring Boot 项目中,我们经常会看到这样的代码:
java
@Async
public void sendMail() {
// 发送邮件
}
或者:
java
@Async("alertThreadPool")
public void alert(String url, Exception e) {
// 发送告警
}
很多初学者都知道:
@Async可以异步执行方法- 不会阻塞当前线程
但是看到下面这种写法时往往会疑惑:
java
@Async("alertThreadPool")
为什么要指定一个线程池?
直接使用 @Async 不行吗?
本文从 @Async 的基础使用开始,一步一步分析为什么企业项目更喜欢使用自定义线程池。
一、@Async 的作用
Spring 提供了 @Async 注解用于实现异步调用。
例如:
java
@Service
public class MailService {
@Async
public void sendMail() {
System.out.println("开始发送邮件");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("邮件发送完成");
}
}
业务代码:
java
mailService.sendMail();
System.out.println("主线程继续执行");
输出:
text
主线程继续执行
开始发送邮件
5秒后...
邮件发送完成
可以看到:
text
主线程
↓
调用 sendMail()
↓
立即返回
异步线程
↓
执行 sendMail()
发送邮件不会阻塞主线程。
二、@Async 底层是怎么工作的?
很多人以为:
java
@Async
只是简单创建一个线程。
实际上不是。
Spring 底层使用的是:
text
线程池(Executor)
例如:
java
@Async
public void sendMail() {
}
Spring 实际执行过程类似:
java
executor.execute(() -> {
sendMail();
});
只是这些代码被 Spring AOP 自动完成了。
三、只使用 @Async 会有什么问题?
假设项目中有多个异步任务:
java
@Async
public void sendMail(){}
@Async
public void sendSms(){}
@Async
public void syncData(){}
@Async
public void generateReport(){}
这些任务默认会共用同一个线程池。
例如:
text
默认线程池
├── 发送邮件
├── 发送短信
├── 数据同步
└── 报表生成
看起来没问题。
但是生产环境经常会出现:
text
数据同步任务非常多
↓
线程池被占满
↓
邮件发送等待
↓
告警发送失败
最终形成:
text
一个业务拖垮整个异步系统
这就是线程池资源争抢问题。
四、为什么要自定义线程池?
企业项目通常会进行资源隔离。
例如:
text
邮件线程池
↓
只负责邮件
短信线程池
↓
只负责短信
报表线程池
↓
只负责报表
告警线程池
↓
只负责告警
这样即使:
text
报表任务爆发
也不会影响:
text
告警任务
因此企业项目经常会看到:
java
@Async("alertThreadPool")
这种写法。
五、自定义线程池示例
开启异步功能
首先开启 Spring 异步支持:
java
@Configuration
@EnableAsync
public class AsyncConfig {
}
创建线程池
java
@Configuration
public class ThreadPoolConfig {
@Bean
public Executor alertThreadPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(1000);
executor.setThreadNamePrefix("alert-thread-pool-");
executor.initialize();
return executor;
}
}
此时 Spring 容器中已经有一个名字叫:
text
alertThreadPool
的线程池 Bean。
使用线程池
java
@Service
public class ErrorClient {
@Async("alertThreadPool")
public void alert(String url, Exception e) {
System.out.println(Thread.currentThread().getName());
alertCore(url, e);
}
private void alertCore(String url, Exception e) {
//发送邮件
//发送企业微信
//发送钉钉消息
}
}
当异步执行 alert 方法时,就会使用 alertThreadPool 方法创建的线程池进行执行。
六、@Async("alertThreadPool") 是怎么找到线程池的?
很多人疑惑:
java
@Async("alertThreadPool")
为什么 Spring 知道要使用哪个线程池?
原因很简单。
因为:
java
@Bean
public Executor alertThreadPool()
默认 Bean 名称就是:
text
alertThreadPool
而:
java
@Async("alertThreadPool")
指定的正是 Bean 名称。
Spring 会自动从容器中找到:
java
Executor alertThreadPool
然后执行:
java
alertThreadPool.execute(...)
所以:
java
@Async("alertThreadPool")
本质上相当于:
java
@Autowired
Executor alertThreadPool;
public void alert(...) {
alertThreadPool.execute(() -> {
alertCore(...);
});
}
只是 Spring 帮我们自动完成了。
七、实际项目案例
例如 Quartz 定时任务:
java
try {
restTemplate.getForObject(url, String.class);
} catch (Exception e) {
errorClient.alert(url, e);
}
如果告警是同步执行:
text
Quartz线程
↓
发送邮件
↓
等待邮件服务器
Quartz 调度线程会被阻塞。
如果使用:
java
@Async("alertThreadPool")
执行流程变成:
text
Quartz线程
↓
提交告警任务
↓
立即返回
alertThreadPool线程
↓
发送邮件
这样:
text
Quartz只负责调度
告警线程负责通知
两者互不影响。
八、线程名称的重要性
建议给线程池设置前缀:
java
executor.setThreadNamePrefix("alert-thread-pool-");
日志中就会显示:
text
alert-thread-pool-1
alert-thread-pool-2
而不是:
text
pool-1-thread-1
pool-1-thread-2
排查问题时非常方便。
九、生产环境注意事项
很多项目会这样配置:
java
executor.setQueueCapacity(
Integer.MAX_VALUE
);
看似安全。
实际上存在风险:
text
任务无限堆积
↓
内存不断增长
↓
OOM
更推荐:
java
executor.setQueueCapacity(1000);
或者:
java
executor.setQueueCapacity(5000);
设置合理上限。
十、总结
@Async("alertThreadPool") 的作用不仅仅是异步执行。
更重要的是:
text
线程池资源隔离
普通写法:
java
@Async
特点:
text
简单
适合小项目
所有异步任务共享线程池
企业项目写法:
java
@Async("alertThreadPool")
特点:
text
指定线程池
资源隔离
互不影响
便于监控
便于排查问题
因此在生产环境中,更推荐为不同业务创建独立线程池,而不是所有异步任务共用一个默认线程池。