通过上一篇:配置@Async异步任务的线程池的介绍,你应该已经了解到异步任务的执行背后有一个线程池来管理执行任务。为了控制异步任务的并发不影响到应用的正常运作,我们必须要对线程池做好相应的配置,防止资源的过渡使用。除了默认线程池的配置之外,还有一类场景,也是很常见的,那就是多任务情况下的线程池隔离。

什么是线程池的隔离,为什么要隔离

可能有的小伙伴还不太了解什么是线程池的隔离,为什么要隔离?。所以,我们先来看看下面的场景案例:

@RestController
public class HelloController { @Autowired
private AsyncTasks asyncTasks; @GetMapping("/api-1")
public String taskOne() {
CompletableFuture<String> task1 = asyncTasks.doTaskOne("1");
CompletableFuture<String> task2 = asyncTasks.doTaskOne("2");
CompletableFuture<String> task3 = asyncTasks.doTaskOne("3"); CompletableFuture.allOf(task1, task2, task3).join();
return "";
} @GetMapping("/api-2")
public String taskTwo() {
CompletableFuture<String> task1 = asyncTasks.doTaskTwo("1");
CompletableFuture<String> task2 = asyncTasks.doTaskTwo("2");
CompletableFuture<String> task3 = asyncTasks.doTaskTwo("3"); CompletableFuture.allOf(task1, task2, task3).join();
return "";
} }

上面的代码中,有两个API接口,这两个接口的具体执行逻辑中都会把执行过程拆分为三个异步任务来实现。

好了,思考一分钟,想一下。如果这样实现,会有什么问题吗?


上面这段代码,在API请求并发不高,同时如果每个任务的处理速度也够快的时候,是没有问题的。但如果并发上来或其中某几个处理过程扯后腿了的时候。这两个提供不相干服务的接口可能会互相影响。比如:假设当前线程池配置的最大线程数有2个,这个时候/api-1接口中task1和task2处理速度很慢,阻塞了;那么此时,当用户调用api-2接口的时候,这个服务也会阻塞!

造成这种现场的原因是:默认情况下,所有用@Async创建的异步任务都是共用的一个线程池,所以当有一些异步任务碰到性能问题的时候,是会直接影响其他异步任务的。

为了解决这个问题,我们就需要对异步任务做一定的线程池隔离,让不同的异步任务互不影响。

不同异步任务配置不同线程池

下面,我们就来实际操作一下!

第一步:初始化多个线程池,比如下面这样:

@EnableAsync
@Configuration
public class TaskPoolConfig { @Bean
public Executor taskExecutor1() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(10);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("executor-1-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
} @Bean
public Executor taskExecutor2() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(10);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("executor-2-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}

注意:这里特地用executor.setThreadNamePrefix设置了线程名的前缀,这样可以方便观察后面具体执行的顺序。

第二步:创建异步任务,并指定要使用的线程池名称

@Slf4j
@Component
public class AsyncTasks { public static Random random = new Random(); @Async("taskExecutor1")
public CompletableFuture<String> doTaskOne(String taskNo) throws Exception {
log.info("开始任务:{}", taskNo);
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任务:{},耗时:{} 毫秒", taskNo, end - start);
return CompletableFuture.completedFuture("任务完成");
} @Async("taskExecutor2")
public CompletableFuture<String> doTaskTwo(String taskNo) throws Exception {
log.info("开始任务:{}", taskNo);
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任务:{},耗时:{} 毫秒", taskNo, end - start);
return CompletableFuture.completedFuture("任务完成");
} }

这里@Async注解中定义的taskExecutor1taskExecutor2就是线程池的名字。由于在第一步中,我们没有具体写两个线程池Bean的名称,所以默认会使用方法名,也就是taskExecutor1taskExecutor2

第三步:写个单元测试来验证下,比如下面这样:

@Slf4j
@SpringBootTest
public class Chapter77ApplicationTests { @Autowired
private AsyncTasks asyncTasks; @Test
public void test() throws Exception {
long start = System.currentTimeMillis(); // 线程池1
CompletableFuture<String> task1 = asyncTasks.doTaskOne("1");
CompletableFuture<String> task2 = asyncTasks.doTaskOne("2");
CompletableFuture<String> task3 = asyncTasks.doTaskOne("3"); // 线程池2
CompletableFuture<String> task4 = asyncTasks.doTaskTwo("4");
CompletableFuture<String> task5 = asyncTasks.doTaskTwo("5");
CompletableFuture<String> task6 = asyncTasks.doTaskTwo("6"); // 一起执行
CompletableFuture.allOf(task1, task2, task3, task4, task5, task6).join(); long end = System.currentTimeMillis(); log.info("任务全部完成,总耗时:" + (end - start) + "毫秒");
} }

在上面的单元测试中,一共启动了6个异步任务,前三个用的是线程池1,后三个用的是线程池2。

先不执行,根据设置的核心线程2和最大线程数2,来分析一下,大概会是怎么样的执行情况?

  1. 线程池1的三个任务,task1和task2会先获得执行线程,然后task3因为没有可分配线程进入缓冲队列
  2. 线程池2的三个任务,task4和task5会先获得执行线程,然后task6因为没有可分配线程进入缓冲队列
  3. 任务task3会在task1或task2完成之后,开始执行
  4. 任务task6会在task4或task5完成之后,开始执行

分析好之后,执行下单元测试,看看是否是这样的:

2021-09-15 23:45:11.369  INFO 61670 --- [   executor-1-1] com.didispace.chapter77.AsyncTasks       : 开始任务:1
2021-09-15 23:45:11.369 INFO 61670 --- [ executor-2-2] com.didispace.chapter77.AsyncTasks : 开始任务:5
2021-09-15 23:45:11.369 INFO 61670 --- [ executor-2-1] com.didispace.chapter77.AsyncTasks : 开始任务:4
2021-09-15 23:45:11.369 INFO 61670 --- [ executor-1-2] com.didispace.chapter77.AsyncTasks : 开始任务:2
2021-09-15 23:45:15.905 INFO 61670 --- [ executor-2-1] com.didispace.chapter77.AsyncTasks : 完成任务:4,耗时:4532 毫秒
2021-09-15 23:45:15.905 INFO 61670 --- [ executor-2-1] com.didispace.chapter77.AsyncTasks : 开始任务:6
2021-09-15 23:45:18.263 INFO 61670 --- [ executor-1-2] com.didispace.chapter77.AsyncTasks : 完成任务:2,耗时:6890 毫秒
2021-09-15 23:45:18.263 INFO 61670 --- [ executor-1-2] com.didispace.chapter77.AsyncTasks : 开始任务:3
2021-09-15 23:45:18.896 INFO 61670 --- [ executor-2-2] com.didispace.chapter77.AsyncTasks : 完成任务:5,耗时:7523 毫秒
2021-09-15 23:45:19.842 INFO 61670 --- [ executor-1-2] com.didispace.chapter77.AsyncTasks : 完成任务:3,耗时:1579 毫秒
2021-09-15 23:45:20.551 INFO 61670 --- [ executor-1-1] com.didispace.chapter77.AsyncTasks : 完成任务:1,耗时:9178 毫秒
2021-09-15 23:45:24.117 INFO 61670 --- [ executor-2-1] com.didispace.chapter77.AsyncTasks : 完成任务:6,耗时:8212 毫秒
2021-09-15 23:45:24.117 INFO 61670 --- [ main] c.d.chapter77.Chapter77ApplicationTests : 任务全部完成,总耗时:12762毫秒

好了,今天的学习就到这里!如果您学习过程中如遇困难?可以加入我们超高质量的Spring技术交流群,参与交流与讨论,更好的学习与进步!更多Spring Boot教程可以点击直达!,欢迎收藏与转发支持!

代码示例

本文的完整工程可以查看下面仓库中2.x目录下的chapter7-7工程:

如果您觉得本文不错,欢迎Star支持,您的关注是我坚持的动力!

欢迎关注我的公众号:程序猿DD,分享外面看不到的干货与思考!

Spring Boot中有多个@Async异步任务时,记得做好线程池的隔离!的更多相关文章

  1. Python进阶----异步同步,阻塞非阻塞,线程池(进程池)的异步+回调机制实行并发, 线程队列(Queue, LifoQueue,PriorityQueue), 事件Event,线程的三个状态(就绪,挂起,运行) ,***协程概念,yield模拟并发(有缺陷),Greenlet模块(手动切换),Gevent(协程并发)

    Python进阶----异步同步,阻塞非阻塞,线程池(进程池)的异步+回调机制实行并发, 线程队列(Queue, LifoQueue,PriorityQueue), 事件Event,线程的三个状态(就 ...

  2. spring boot 学习(十一)使用@Async实现异步调用

    使用@Async实现异步调用 什么是”异步调用”与”同步调用” “同步调用”就是程序按照一定的顺序依次执行,,每一行程序代码必须等上一行代码执行完毕才能执行:”异步调用”则是只要上一行代码执行,无需等 ...

  3. Asynchronous Streaming Request Processing in Spring MVC 4.2 + Spring Boot(SpringBoot中处理异步流请求 SpringMvc4.2以上)

    With the release of Spring 4.2 version, Three new classes have been introduced to handle Requests As ...

  4. spring boot:使用mybatis访问多个mysql数据源/查看Hikari连接池的统计信息(spring boot 2.3.1)

    一,为什么要访问多个mysql数据源? 实际的生产环境中,我们的数据并不会总放在一个数据库, 例如:业务数据库:存放了用户/商品/订单 统计数据库:按年.月.日的针对用户.商品.订单的统计表 因为统计 ...

  5. 【Azure 应用服务】App Service For Linux 部署Java Spring Boot应用后,查看日志文件时的疑惑

    编写Java Spring Boot应用,通过配置logging.path路径把日志输出在指定的文件夹中. 第一步:通过VS Code创建一个空的Spring Boot项目 第二步:在applicat ...

  6. 使用@Async注解创建多线程,自定义线程池

    说明 使用@Async注解创建多线程非常的方便,还可以通过配置,实现线程池.比直接使用线程池简单太多.而且在使用上跟普通方法没什么区别,加上个@Async注解即可实现异步调用. 用法 AsyncTas ...

  7. 【快学springboot】10.使用@Async注解创建多线程,自定义线程池

    说明 使用@Async注解创建多线程非常的方便,还可以通过配置,实现线程池.比直接使用线程池简单太多.而且在使用上跟普通方法没什么区别,加上个@Async注解即可实现异步调用. 用法 AsyncTas ...

  8. 创建spring boot项目并添加多个模块时,启动报 错误: 找不到或无法加载主类

          最近建个项目发现启动报,找不到或无法加载主类,想想肯定是自己配置出问题了,经过排查确实出问题了,(根pom中的bulid为移到子模块中去导致的),下面演示下正确的创建子模块的步奏 1. 创 ...

  9. Spring boot 项目中put提交Date数据时出现type=Bad Request, status=400状态码

    1.问题原因 经过测试发现,当客户端页面提交日期为空时会出现以下异常,如果提交日期不为空则不会出现上述问题.出现这种错误的原因是没有对代码中的Date型参数进行格式化,接收为null的日期类型参数时, ...

随机推荐

  1. 我一个五年Android开发,居然被一个技术不如我的面试官嫌弃了......

    背景 首先介绍一下自己的情况.目前所在的是一家小的创业公司,待了5年多,薪资一般吧.由于这几年公司也在转型.工作经历大概可以分为 3 个阶段. 第一阶段是从进公司开始做 android app 开发, ...

  2. Linux从头学07:中断那么重要,它的本质到底是什么?

    作 者:道哥,10+年的嵌入式开发老兵. 公众号:[IOT物联网小镇],专注于:C/C++.Linux操作系统.应用程序设计.物联网.单片机和嵌入式开发等领域. 公众号回复[书籍],获取 Linux. ...

  3. 跟我一起写 Makefile(五)

    六.多目标 Makefile的规则中的目标可以不止一个,其支持多目标,有可能我们的多个目标同时依赖于一个文件,并且其生成的命令大体类似.于是我们就能把其合并起来.当然,多个目标的生成规则的执行命令是同 ...

  4. TreeUtil.java

    package com.infish.util; import java.lang.reflect.Method; import java.util.ArrayList; import java.ut ...

  5. 【编程语言】Matlab 学习记录

    title: Matlab Learning Record date: 2020-05-23 20:11:26 author: liudongdong1 img: https://gitee.com/ ...

  6. springboot中redis取缓存类型转换异常

    异常如下: [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested ...

  7. new一个对象的时候,实际做了些什么

    当我们说new一个对象的时候,实际做了些什么, 可以参考下图理解

  8. 4种Golang并发操作中常见的死锁情形

    摘要:什么是死锁,在Go的协程里面死锁通常就是永久阻塞了,你拿着我的东西,要我先给你然后再给我,我拿着你的东西又让你先给我,不然就不给你.我俩都这么想,这事就解决不了了. 本文分享自华为云社区< ...

  9. 经典深度学习CNN总结 - LeNet、AlexNet、GoogLeNet、VGG、ResNet

    参考了: https://www.cnblogs.com/52machinelearning/p/5821591.html https://blog.csdn.net/qq_24695385/arti ...

  10. MutationObserver API

    1.概述 MutationObserver接口提供了监视对DOM树所做更改的能力.它被设计为旧的Mutation Events功能的替代品,该功能是DOM3 Events规范的一部分. 但是,它与Mu ...