Spring Boot中使用@Async的时候,千万别忘了线程池的配置!
上一篇我们介绍了如何使用@Async注解来创建异步任务,我可以用这种方法来实现一些并发操作,以加速任务的执行效率。但是,如果只是如前文那样直接简单的创建来使用,可能还是会碰到一些问题。存在有什么问题呢?先来思考下,下面的这个接口,通过异步任务加速执行的实现,是否存在问题或风险呢?
@RestController
public class HelloController {
@Autowired
private AsyncTasks asyncTasks;
@GetMapping("/hello")
public String hello() {
// 将可以并行的处理逻辑,拆分成三个异步任务同时执行
CompletableFuture<String> task1 = asyncTasks.doTaskOne();
CompletableFuture<String> task2 = asyncTasks.doTaskTwo();
CompletableFuture<String> task3 = asyncTasks.doTaskThree();
CompletableFuture.allOf(task1, task2, task3).join();
return "Hello World";
}
}
虽然,从单次接口调用来说,是没有问题的。但当接口被客户端频繁调用的时候,异步任务的数量就会大量增长:3 x n(n为请求数量),如果任务处理不够快,就很可能会出现内存溢出的情况。那么为什么会内存溢出呢?根本原因是由于Spring Boot默认用于异步任务的线程池是这样配置的:

图中我标出的两个重要参数是需要关注的:
queueCapacity:缓冲队列的容量,默认为INT的最大值(2的31次方-1)。maxSize:允许的最大线程数,默认为INT的最大值(2的31次方-1)。
所以,默认情况下,一般任务队列就可能把内存给堆满了。所以,我们真正使用的时候,还需要对异步任务的执行线程池做一些基础配置,以防止出现内存溢出导致服务不可用的问题。
配置默认线程池
默认线程池的配置很简单,只需要在配置文件中完成即可,主要有以下这些参数:
spring.task.execution.pool.core-size=2
spring.task.execution.pool.max-size=5
spring.task.execution.pool.queue-capacity=10
spring.task.execution.pool.keep-alive=60s
spring.task.execution.pool.allow-core-thread-timeout=true
spring.task.execution.shutdown.await-termination=false
spring.task.execution.shutdown.await-termination-period=
spring.task.execution.thread-name-prefix=task-
具体配置含义如下:
spring.task.execution.pool.core-size:线程池创建时的初始化线程数,默认为8spring.task.execution.pool.max-size:线程池的最大线程数,默认为int最大值spring.task.execution.pool.queue-capacity:用来缓冲执行任务的队列,默认为int最大值spring.task.execution.pool.keep-alive:线程终止前允许保持空闲的时间spring.task.execution.pool.allow-core-thread-timeout:是否允许核心线程超时spring.task.execution.shutdown.await-termination:是否等待剩余任务完成后才关闭应用spring.task.execution.shutdown.await-termination-period:等待剩余任务完成的最大时间spring.task.execution.thread-name-prefix:线程名的前缀,设置好了之后可以方便我们在日志中查看处理任务所在的线程池
动手试一试
我们直接基于之前chapter7-5的结果来进行如下操作。
首先,在没有进行线程池配置之前,可以先执行一下单元测试:
@Test
public void test1() throws Exception {
long start = System.currentTimeMillis();
CompletableFuture<String> task1 = asyncTasks.doTaskOne();
CompletableFuture<String> task2 = asyncTasks.doTaskTwo();
CompletableFuture<String> task3 = asyncTasks.doTaskThree();
CompletableFuture.allOf(task1, task2, task3).join();
long end = System.currentTimeMillis();
log.info("任务全部完成,总耗时:" + (end - start) + "毫秒");
}
由于默认线程池的核心线程数是8,所以3个任务会同时开始执行,日志输出是这样的:
2021-09-15 00:30:14.819 INFO 77614 --- [ task-2] com.didispace.chapter76.AsyncTasks : 开始做任务二
2021-09-15 00:30:14.819 INFO 77614 --- [ task-3] com.didispace.chapter76.AsyncTasks : 开始做任务三
2021-09-15 00:30:14.819 INFO 77614 --- [ task-1] com.didispace.chapter76.AsyncTasks : 开始做任务一
2021-09-15 00:30:15.491 INFO 77614 --- [ task-2] com.didispace.chapter76.AsyncTasks : 完成任务二,耗时:672毫秒
2021-09-15 00:30:19.496 INFO 77614 --- [ task-3] com.didispace.chapter76.AsyncTasks : 完成任务三,耗时:4677毫秒
2021-09-15 00:30:20.443 INFO 77614 --- [ task-1] com.didispace.chapter76.AsyncTasks : 完成任务一,耗时:5624毫秒
2021-09-15 00:30:20.443 INFO 77614 --- [ main] c.d.chapter76.Chapter76ApplicationTests : 任务全部完成,总耗时:5653毫秒
接着,可以尝试在配置文件中增加如下的线程池配置
spring.task.execution.pool.core-size=2
spring.task.execution.pool.max-size=5
spring.task.execution.pool.queue-capacity=10
spring.task.execution.pool.keep-alive=60s
spring.task.execution.pool.allow-core-thread-timeout=true
spring.task.execution.thread-name-prefix=task-
日志输出的顺序会变成如下的顺序:
2021-09-15 00:31:50.013 INFO 77985 --- [ task-1] com.didispace.chapter76.AsyncTasks : 开始做任务一
2021-09-15 00:31:50.013 INFO 77985 --- [ task-2] com.didispace.chapter76.AsyncTasks : 开始做任务二
2021-09-15 00:31:52.452 INFO 77985 --- [ task-1] com.didispace.chapter76.AsyncTasks : 完成任务一,耗时:2439毫秒
2021-09-15 00:31:52.452 INFO 77985 --- [ task-1] com.didispace.chapter76.AsyncTasks : 开始做任务三
2021-09-15 00:31:55.880 INFO 77985 --- [ task-2] com.didispace.chapter76.AsyncTasks : 完成任务二,耗时:5867毫秒
2021-09-15 00:32:00.346 INFO 77985 --- [ task-1] com.didispace.chapter76.AsyncTasks : 完成任务三,耗时:7894毫秒
2021-09-15 00:32:00.347 INFO 77985 --- [ main] c.d.chapter76.Chapter76ApplicationTests : 任务全部完成,总耗时:10363毫秒
- 任务一和任务二会马上占用核心线程,任务三进入队列等待
- 任务一完成,释放出一个核心线程,任务三从队列中移出,并占用核心线程开始处理
注意:这里可能有的小伙伴会问,最大线程不是5么,为什么任务三是进缓冲队列,不是创建新线程来处理吗?这里要理解缓冲队列与最大线程间的关系:只有在缓冲队列满了之后才会申请超过核心线程数的线程来进行处理。所以,这里只有缓冲队列中10个任务满了,再来第11个任务的时候,才会在线程池中创建第三个线程来处理。这个这里就不具体写列子了,读者可以自己调整下参数,或者调整下单元测试来验证这个逻辑。
本系列教程《Spring Boot 2.x基础教程》点击直达!,欢迎收藏与转发!如果学习过程中如遇困难?可以加入我们Spring技术交流群,参与交流与讨论,更好的学习与进步!
代码示例
本文的完整工程可以查看下面仓库中2.x目录下的chapter7-6工程:
- Github:https://github.com/dyc87112/SpringBoot-Learning/
- Gitee:https://gitee.com/didispace/SpringBoot-Learning/
如果您觉得本文不错,欢迎Star支持,您的关注是我坚持的动力!
欢迎关注我的公众号:程序猿DD,分享外面看不到的干货与思考!
Spring Boot中使用@Async的时候,千万别忘了线程池的配置!的更多相关文章
- spring boot中使用@Async实现异步调用任务
本篇文章主要介绍了spring boot中使用@Async实现异步调用任务,小编觉得挺不错的,现在分享给大家,也给大家做个参考.一起跟随小编过来看看吧 什么是“异步调用”? “异步调用”对应的是“同步 ...
- Spring Boot中使用@Async实现异步调用
在Spring Boot中,我们只需要通过使用@Async注解就能简单的将原来的同步函数变为异步函数,为了让@Async注解能够生效,还需要在Spring Boot的主程序中配置@EnableAsyn ...
- 56. spring boot中使用@Async实现异步调用【从零开始学Spring Boot】
什么是"异步调用"? "异步调用"对应的是"同步调用",同步调用指程序按照定义顺序依次执行,每一行程序都必须等待上一行程序执行完成之后才能执 ...
- Spring Boot中使用@Async实现异步调用,加速任务的执行!
什么是"异步调用"?"异步调用"对应的是"同步调用",同步调用指程序按照定义顺序依次执行,每一行程序都必须等待上一行程序执行完成之后才能执行 ...
- Spring Boot中的Properties
文章目录 简介 使用注解注册一个Properties文件 使用属性文件 Spring Boot中的属性文件 @ConfigurationProperties yaml文件 Properties环境变量 ...
- Spring Boot中实现异步调用之@Async
一.什么是异步调用 “异步调用”对应的是“同步调用”,同步调用指程序按照定义顺序依次执行,每一行程序都必须等待上一行程序执行完成之后才能执行:异步调用指程序在顺序执行时,不等待异步调用 的语句返回结果 ...
- 【spring boot】spring boot中使用定时任务配置
spring boot中使用定时任务配置 =============================================================================== ...
- Spring Boot 中如何支持异步方法
本人免费整理了Java高级资料,涵盖了Java.Redis.MongoDB.MySQL.Zookeeper.Spring Cloud.Dubbo高并发分布式等教程,一共30G,需要自己领取.传送门:h ...
- Spring Boot中有多个@Async异步任务时,记得做好线程池的隔离!
通过上一篇:配置@Async异步任务的线程池的介绍,你应该已经了解到异步任务的执行背后有一个线程池来管理执行任务.为了控制异步任务的并发不影响到应用的正常运作,我们必须要对线程池做好相应的配置,防止资 ...
随机推荐
- [源码解析] 机器学习参数服务器 Paracel (2)--------SSP控制协议实现
[源码解析] 机器学习参数服务器 Paracel (2)-----SSP实现 目录 [源码解析] 机器学习参数服务器 Paracel (2)-----SSP实现 0x00 摘要 0x01 背景知识 1 ...
- 【SpringBoot技术专题】「JWT技术专区」SpringSecurity整合JWT授权和认证实现
JWT基本概念 JWT,即 JSON Web Tokens(RFC 7519),是一个广泛用于验证 REST APIs 的标准.虽说是一个新兴技术,但它却得以迅速流行. JWT的验证过程是: 前端(客 ...
- 如何发送一个http请求—apipost
API界面功能布局 API请求参数 Header 参数 你可以设置或者导入 Header 参数,cookie也在Header进行设置 Query 参数 Query 支持构造URL参数,同时支持 RES ...
- 【原创】Spring Data Redis <=2.0.3反序列化漏洞
Spring Data Redis隶属于Spring Data家族, 提供简单易用的方式来访问Redis缓存. Spring Data Redis在往Redis里面写数据的时候,默认会先对数据进行序列 ...
- Python - pydantic 入门介绍与 Models 的简单使用
前言 为啥要学这个,因为 FastAPI 是基于它进行开发的,而且是个不错的框架,所以有必要深入学习 前置学习 Python 类型提示:https://www.cnblogs.com/poloyy/p ...
- C++类的构造函数后面加一个冒号的含义
最近在看侯捷老师讲解的C++知识,他讲到在构造函数加一个冒号初始化值的方法,会使代码更大气,在此记录一下使用方法 构造函数冒号后的初始化class complex{public: complex(do ...
- 【Azure 应用服务】使用PowerShell脚本上传文件至App Service目录
问题描述 使用PowerShell脚本上传文件至App Service目录的示例 脚本示例 对文件进行上传,使用的 WebClient.UploadFile 方法进行上传.当文件夹中包含子目录,执行以 ...
- 虚拟dom?diff算法?key?Vue原理的核心三问?打包教你搞定。
为什么需要虚拟DOM 先介绍浏览器加载一个HTML文件需要做哪些事,帮助我们理解为什么我们需要虚拟DOM.webkit引擎的处理流程,如下图所示: 所有浏览器的引擎工作流程都差不多,如上图大致分5步: ...
- rabbitMq可靠性投递之配置(消息至交换机,至队列不通的回调)
@Bean public RabbitTemplate rabbitTemplate(CachingConnectionFactory factory) { //若使用confirm-callback ...
- java.lang.NullPointerException at org.apache.jsp.index_jsp._jspInit(index_jsp.java:40)
做JSP页面时,出现如下错误,把如上面依赖下面一个去除即可