Springboot 如何使用@Async整合线程池
Springboot @Async整合线程池
开篇咱们先来聊聊线程池这个概念,或者说为什么要使用线程池;简言之,充分利用cpu资源,提高程序执行时间,但是相反,线程池异常提示、主线程和子线程事务问题也是显而易见的。
那么@Async这个注解又是什么做用呢?其实就是标识方法为异步任务的一个注解,默认会自己维护一个线程池(存在弊端),利用子线程去执行任务;那么如果把这两者结合的话,线程池+Async又会有什么效果呢!
循序渐进
提到线程池,可以采用Executors提供四种线程池下,使用某些特性的场景下,还是不错的(简便省事),当然此篇文章就不进行描述,我们一块来看看自定义线程池的配置,具体参数自行查阅,需要注意“核心线程数”、“最大线程数”、“线程队列数值”配置:
接下来便要将Async和线程池一块使用了,使用之前请先到springboot启动类上加上@EnableAsync注解,具体使用方式如下:
此时便是开始在主线程里面通过多线程来使用异步任务,不过此时也需要分情况考虑,这里便提前和大家一块说了。
1:异步任务是否和主线程有关联,简言之,就是两者之间是否相互不影响?
2:如果不影响的话,主线程和异步任务的子线程直接使用便可以;
3:如果影响的话,主线程是要等待子线程执行完的结果的,此时便可以考虑加锁、或者使用一些提供好的并发类,比如CountDownLatch、CyclicBarrier,两者的区别请自行查阅,结合项目中的需求,我这里使用的前者CountDownLatch 。
如果没有其它业务需求的话,那么此时到这里便可以正常使用了,但是如果碰到子线程中有一些“自定义的提示信息”,或者是“自定义的异常信息”,如果单纯的考虑在主线程中通过Try…catch,或者SpringMVC中全局异常拦截**@RestControllerAdvice**能够进行处理,那就大错特错了,根本不可能实现的;那么此时我们便需要这样使用,才可以让主线程中捕获信息,然后返回给接口调用方-前端。
但是你以为这样就结束了么,相对于上面的操作,我们可以将子线程是否需要返回提示信息分为以下两种情况:
1:需要返回,子线程抛异常后,通过主线程返回提示信息,那么采用上面图片的处理方式
2:不需要返回,子线程抛异常后,要在某些地方进行日志记录等等,此时可以换一种方式定义线程池,具体方式如下,本人亲自测试过。
难道这篇博客的介绍就此结束了,休想,强行灌输一波,主线程和多线程中的子线程事务问题,具体情况如下:
1:主线程异常、子线程未执行,此情况不会有任何影响
2:主线程先执行,子线程异常,此时会有影响;
2.1:主线程中加入@Transactional(rollbackFor = Exception.class)注解,子线程不加事务注解,此时如果都正常的话,那么主线程和子线程在同一事务里面,一块提交;反之子线程如果不正常的话,那么主线程和子线程都不会进行提交;
2.2:主线程加入事务注解,子线程中也加入事务注解@Transactional(propagation = Propagation.REQUIRES_NEW),如果都正常的话,子线程提交后主线程才会提交;反之主线程和子线程都不会进行提交。
问题思考
其实在上面设计过程中,偏于实践操作的部分多一些,当然如果会一些理论知识会更好理解的;在项目中使用多线程技术后,发现多线程其实也并没有那么难理解,更多的都是在定义方面,这点也很好解决,反复理解和思考就行了;
个人认为,利用一些锁的方式来解决主线程和子线程协同的问题会更帅一些 !
SpringBoot线程池的创建、@Async配置步骤及注意事项
最近在做订单模块,用户购买服务类产品之后,需要进行预约,预约成功之后分别给商家和用户发送提醒短信。考虑发短信耗时的情况所以我想用异步的方法去执行,于是就在网上看见了Spring的@Async了。
但是遇到了许多问题,使得@Async无效,也一直没有找到很好的文章去详细的说明@Async的正确及错误的使用方法及需要注意的地方,这里简单整理了一下遇见的问题,Sring是以配置文件的形式来开启@Async,而SpringBoot则是以注解的方式开启。
我们可以使用springBoot默认的线程池,不过一般我们会自定义线程池(因为比较灵活),配置方式有:
1、使用 xml 文件配置的方式
2、使用Java代码结合@Configuration进行配置(推荐使用)
下面分别实现两种配置方式
第一步、配置@Async
一、springBoot启动类的配置:
在Spring Boot的主程序中配置@EnableAsync,如下所示:
@ServletComponentScan @SpringBootApplication @EnableAsync public class ClubApiApplication { public static void main(String[] args) { SpringApplication.run(ClubApiApplication.class, args); } }
二、Spring XML的配置方式:
1.applicationContext.xml同目录下创建文件threadPool.xml文件:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd"> <!-- 开启异步,并引入线程池 --> <task:annotation-driven executor="threadPool" /> <!-- 定义线程池 --> <bean id="threadPool" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"> <!-- 核心线程数,默认为1 --> <property name="corePoolSize" value="10" /> <!-- 最大线程数,默认为Integer.MAX_VALUE --> <property name="maxPoolSize" value="50" /> <!-- 队列最大长度,一般需要设置值>=notifyScheduledMainExecutor.maxNum;默认为Integer.MAX_VALUE --> <property name="queueCapacity" value="100" /> <!-- 线程池维护线程所允许的空闲时间,默认为60s --> <property name="keepAliveSeconds" value="30" /> <!-- 完成任务自动关闭 , 默认为false--> <property name="waitForTasksToCompleteOnShutdown" value="true" /> <!-- 核心线程超时退出,默认为false --> <property name="allowCoreThreadTimeOut" value="true" /> <!-- 线程池对拒绝任务(无线程可用)的处理策略,目前只支持AbortPolicy、CallerRunsPolicy;默认为后者 --> <property name="rejectedExecutionHandler"> <!-- AbortPolicy:直接抛出java.util.concurrent.RejectedExecutionException异常 --> <!-- CallerRunsPolicy:主线程直接执行该任务,执行完之后尝试添加下一个任务到线程池中,可以有效降低向线程池内添加任务的速度 --> <!-- DiscardOldestPolicy:抛弃旧的任务、暂不支持;会导致被丢弃的任务无法再次被执行 --> <!-- DiscardPolicy:抛弃当前任务、暂不支持;会导致被丢弃的任务无法再次被执行 --> <bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy" /> </property> </bean> </beans>
2.然后在applicationContext.xml中引入threadPool.xml:<import resource="threadPool.xml" />
<!--如果不使用自定义线程池,可以直接使用下面这段标签--> <!-- <task:executor id="WhifExecutor" pool-size="10"/> --> <import resource="threadPool.xml" /> <task:annotation-driven executor="WhifExecutor" />
第二步:创建两个异步方法的类,如下所示
第一个类(这里模拟取消订单后发短信,有两个发送短信的方法):
@Service public class TranTest2Service { // 发送提醒短信 1 @Async public void sendMessage1() throws InterruptedException { System.out.println("发送短信方法---- 1 执行开始"); Thread.sleep(5000); // 模拟耗时 System.out.println("发送短信方法---- 1 执行结束"); } // 发送提醒短信 2 @Async public void sendMessage2() throws InterruptedException { System.out.println("发送短信方法---- 2 执行开始"); Thread.sleep(2000); // 模拟耗时 System.out.println("发送短信方法---- 2 执行结束"); } }
第二个类。调用发短信的方法 (异步方法不能与被调用的异步方法在同一个类中,否则无效):
@Service public class OrderTaskServic { @Autowired private TranTest2Service tranTest2Service; // 订单处理任务 public void orderTask() throws InterruptedException { this.cancelOrder(); // 取消订单 tranTest2Service.sendMessage1(); // 发短信的方法 1 tranTest2Service.sendMessage2(); // 发短信的方法 2 } // 取消订单 public void cancelOrder() throws InterruptedException { System.out.println("取消订单的方法执行------开始"); System.out.println("取消订单的方法执行------结束 "); } }
经过测试得到如下结果:
1.没有使用@Async
2.使用了@Async
可以看出,没有使用@Async方式实现的发送短信是同步执行的,意思就是说第一条发送之后再发送第二条,第二条发送成功之后再给用户提示,这样显然会影响用户体验,再看使用了@Async实现的,在执行第一个发送短信方法之后马上开启另一个线程执行第二个方法,显然这样我们的处理速度回快很多。
使用Java代码结合@Configuration注解的配置方式(推荐使用)
1. 新建一个配置类
package com.boot.common.conf; import java.util.concurrent.ThreadPoolExecutor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; /** * 线程池配置 * @author zhh * */ @Configuration @EnableAsync public class ThreadPoolTaskConfig { /** * 默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务, * 当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中; * 当队列满了,就继续创建线程,当线程数量大于等于maxPoolSize后,开始使用拒绝策略拒绝 */ /** 核心线程数(默认线程数) */ private static final int corePoolSize = 20; /** 最大线程数 */ private static final int maxPoolSize = 100; /** 允许线程空闲时间(单位:默认为秒) */ private static final int keepAliveTime = 10; /** 缓冲队列大小 */ private static final int queueCapacity = 200; /** 线程池名前缀 */ private static final String threadNamePrefix = "Async-Service-"; @Bean("taskExecutor") // bean的名称,默认为首字母小写的方法名 public ThreadPoolTaskExecutor taskExecutor(){ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(corePoolSize); executor.setMaxPoolSize(maxPoolSize); executor.setQueueCapacity(queueCapacity); executor.setKeepAliveSeconds(keepAliveTime); executor.setThreadNamePrefix(threadNamePrefix); // 线程池对拒绝任务的处理策略 // CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 初始化 executor.initialize(); return executor; } }
2.创建两个异步方法的类(和之前的类类似仅仅是方法上注解不一样),如下所示:
第一个类(这里模拟取消订单后发短信,有两个发送短信的方法):
package com.boot.test1.service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @Service public class TranTest2Service { Logger log = LoggerFactory.getLogger(TranTest2Service.class); // 发送提醒短信 1 @PostConstruct // 加上该注解项目启动时就执行一次该方法 @Async("taskExecutor") public void sendMessage1() throws InterruptedException { log.info("发送短信方法---- 1 执行开始"); Thread.sleep(5000); // 模拟耗时 log.info("发送短信方法---- 1 执行结束"); } // 发送提醒短信 2 @PostConstruct // 加上该注解项目启动时就执行一次该方法 @Async("taskExecutor") public void sendMessage2() throws InterruptedException { log.info("发送短信方法---- 2 执行开始"); Thread.sleep(2000); // 模拟耗时 log.info("发送短信方法---- 2 执行结束"); } }
代码中的 @Async("taskExecutor") 对应我们自定义线程池中的 @Bean("taskExecutor") ,表示使用我们自定义的线程池。
第二个类。调用发短信的方法 (异步方法不能与被调用的异步方法在同一个类中,否则无效):
@Service public class OrderTaskServic { @Autowired private TranTest2Service tranTest2Service; // 订单处理任务 public void orderTask() throws InterruptedException { this.cancelOrder(); // 取消订单 tranTest2Service.sendMessage1(); // 发短信的方法 1 tranTest2Service.sendMessage2(); // 发短信的方法 2 } // 取消订单 public void cancelOrder() throws InterruptedException { System.out.println("取消订单的方法执行------开始"); System.out.println("取消订单的方法执行------结束 "); } }
运行截图:
注意看,截图中的 [nio-8090-exec-1] 是Tomcat的线程名称
[Async-Service-1]、[Async-Service-2]表示线程1和线程2 ,是我们自定义的线程池里面的线程名称,我们在配置类里面定义的线程池前缀:
private static final String threadNamePrefix = "Async-Service-"; // 线程池名前缀,说明我们自定义的线程池被使用了
注意事项
如下方式会使@Async失效
- 异步方法使用static修饰
- 异步类没有使用@Component注解(或其他注解)导致spring无法扫描到异步类
- 异步方法不能与被调用的异步方法在同一个类中
- 类中需要使用@Autowired或@Resource等注解自动注入,不能自己手动new对象
- 如果使用SpringBoot框架必须在启动类中增加@EnableAsync注解
以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。