Spring Boot 为什么要使用自定义线程池?一文搞懂线程池的作用
在开发 Spring Boot 项目时,我们经常会遇到一些耗时但不影响主业务的操作,例如:
- 发送邮件
- 发送短信
- 阅读量统计
- 操作日志记录
- 用户行为统计
- 消息通知
很多同学第一反应就是开启一个线程去执行:
new Thread(() -> {
// 执行耗时任务
}).start();
功能虽然可以实现,但在实际项目中,这种写法并不推荐。
那么,为什么 Spring 官方更推荐使用线程池?又为什么很多项目都会配置自定义线程池?
本文带你一步一步理解。
一、什么是线程?
线程(Thread)是程序执行的最小单位。
一个应用程序启动后,会创建一个进程,而一个进程中可以包含多个线程。
例如浏览器:
Chrome
│
├── 页面渲染线程
├── JavaScript线程
├── 网络请求线程
├── GPU线程
└── 定时器线程
多个线程可以同时执行不同的任务,因此能够提高程序的并发能力。
二、为什么要使用线程?
假设有一个接口:
@GetMapping("/article/{id}")
public ArticleVO detail(Long id){
return articleService.detail(id);
}
除了查询文章之外,还需要:
- 阅读量 +1
- 保存阅读历史
- 记录访问日志
如果全部同步执行:
查询文章
↓
更新阅读量
↓
记录日志
↓
保存历史
↓
返回页面
用户必须等待所有任务执行完成。
实际上:
用户真正关心的是:
尽快看到文章内容。
而阅读量统计、日志记录等业务完全可以放到后台执行。
这就是线程存在的意义。
主线程负责处理核心业务。
后台线程负责处理耗时任务。
整个接口响应速度会更快。
三、为什么不能一直 new Thread()?
很多初学者都会这样写:
new Thread(() -> {
sendMail();
}).start();
如果只是偶尔执行一次,问题不大。
但是如果网站一天有几万甚至几十万次请求:
10000 个请求
↓
10000 个 Thread
系统就需要不断地:
创建线程
↓
执行任务
↓
销毁线程
线程的创建和销毁都需要消耗系统资源。
大量线程甚至可能导致:
- CPU 使用率过高
- 内存占用增加
- 上下文切换频繁
- 系统响应变慢
因此,大型项目几乎不会频繁创建线程。
四、什么是线程池?
线程池(Thread Pool)可以理解为:
提前准备好一批线程,需要执行任务时直接拿来使用。
例如线程池中提前创建了四个线程:
Thread-1
Thread-2
Thread-3
Thread-4
当任务到来时:
任务1 → Thread-1
任务2 → Thread-2
任务3 → Thread-3
任务4 → Thread-4
如果线程都在忙:
任务5
↓
进入等待队列
等有线程空闲后,再继续执行。
线程不会反复创建,而是不断复用。
五、线程池有什么优势?
1、减少线程创建和销毁开销
线程创建是一项比较昂贵的操作。
线程池通过重复利用已有线程,大大降低了系统开销。
2、提高系统响应速度
线程准备好以后,任务无需等待线程创建。
直接提交即可执行。
因此整体响应速度更快。
3、统一管理线程数量
线程池可以限制:
- 核心线程数
- 最大线程数
避免因为线程无限增长导致服务器资源耗尽。
例如:
最大线程数:8
即使同时来了 1000 个任务,也不会创建 1000 个线程。
4、提供任务等待队列
当线程全部处于忙碌状态时:
新的任务不会立即丢失。
而是进入等待队列:
任务
↓
BlockingQueue
↓
等待线程执行
当然,如果:
- 队列满了
- 最大线程也满了
新的任务就会触发线程池的拒绝策略。
因此线程池参数需要根据业务合理配置。
六、为什么要使用自定义线程池?
Spring Boot 默认也提供了线程池。
但是实际开发中,通常不会所有业务共用一个线程池。
例如:
发送邮件
阅读量统计
图片压缩
AI生成内容
短信发送
如果全部使用同一个线程池:
某个业务大量占用线程之后,其它业务都会受到影响。
因此一般都会按照业务划分线程池。
例如:
articleThreadPool
mailThreadPool
logThreadPool
imageThreadPool
这样即使:
邮件发送出现大量积压
也不会影响:
文章阅读量统计
业务之间互不影响。
七、Spring Boot 配置自定义线程池
Spring Boot 推荐使用 ThreadPoolTaskExecutor。
例如:
@Configuration
@EnableAsync
public class ThreadPoolConfig {
@Bean("articleThreadPool")
public Executor articleThreadPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数
executor.setCorePoolSize(4);
// 最大线程数
executor.setMaxPoolSize(8);
// 队列容量
executor.setQueueCapacity(100);
// 线程名称前缀
executor.setThreadNamePrefix("article-");
executor.initialize();
return executor;
}
}
随后在需要异步执行的方法上指定线程池:
@Async("articleThreadPool")
public void increaseReadCount(Long articleId){
// 阅读量 +1
}
这样,Spring 就会自动将该任务提交到指定线程池执行。
八、什么时候适合使用线程池?
线程池比较适合处理:
- 阅读量统计
- 登录日志
- 操作日志
- 消息通知
- 邮件发送
- 短信发送
- 文件上传
- 图片压缩
- Excel 导入导出
这些任务有一个共同特点:
不是用户必须立即得到结果。
因此可以放到后台执行,提高接口响应速度。
九、什么时候不建议异步?
如果业务之间存在强依赖关系,就不要异步。
例如:
创建订单
↓
扣减库存
↓
支付
↓
返回成功
如果库存扣减失败,订单就不能继续创建。
这类核心业务流程应该同步执行,保证数据一致性。
十、总结
线程并不是为了让代码"看起来高级",而是为了让耗时任务不阻塞主业务。
相比频繁创建线程,线程池能够:
- 重复利用线程,降低资源开销;
- 控制并发数量,避免服务器资源耗尽;
- 提供任务队列,提高系统稳定性;
- 统一管理线程,方便维护。
在 Spring Boot 项目中,更推荐根据业务划分自定义线程池,而不是所有异步任务共用一个线程池。
例如文章阅读量统计、邮件发送、日志记录等场景,都非常适合交给线程池异步处理。
下一篇文章,我将结合 Spring Event + 自定义线程池,实现一个更加解耦的文章阅读量异步统计功能。