所谓异步任务,其实就是异步执行程序,有些时候遇到一些耗时的的任务,如果一直卡等待,肯定会影响其他程序的执行,所以就让这些程序需要以异步的方式去执行。那么下面就来介绍Spring Boot 如何实现异步任务。
Spring中用@Async注解标记的方法,称为异步方法。在spring boot应用中使用@Async很简单:
- 调用异步方法类上或者启动类加上注解@EnableAsync
- 在需要被异步调用的方法外加上@Async
- 所使用的@Async注解方法的类对象应该是Spring容器管理的bean对象
注解配置开启
在springboot启动类(或是配置类)上添加 @EnableAsync 注解
@EnableAsyncpublicclassSpringBootApplication{}
基于@Async无返回值调用
/**
* 没有返回值的Async方法
*/@AsyncpublicvoidasyncMethodWithVoidReturnType(){
log.info("没有返回值的Async方法, ThreadName : {}",Thread.currentThread().getName());}
基于@Async返回值的调用
/**
* 有返回值的Async方法
* @return Future new AsyncResult
*/@Override@AsyncpublicFuture<String>asyncMethodWithReturnType(){
log.info("有返回值的Async方法, ThreadName : {}",Thread.currentThread().getName());try{Thread.sleep(5000);returnnewAsyncResult<String>("hello world !!!!");}catch(InterruptedException e){
log.error("出问题了, {}", e.getMessage());}returnnull;}
以上示例可以发现,返回的数据类型为Future类型,其为一个接口。具体的结果类型为 AsyncResult,这个是需要注意的地方。
调用返回结果的异步方法示例:
@GetMapping("/future")publicStringfutureAsync()throwsExecutionException,InterruptedException{StringBuilder stringBuilder =newStringBuilder();
stringBuilder.append("Invoking an asynchronous method. threadName : "+Thread.currentThread().getName());Future<String> stringFuture = asyncService.asyncMethodWithReturnType();while(true){if(stringFuture.isDone()){
stringBuilder.append("Result from asynchronous process - "+ stringFuture.get());break;}
stringBuilder.append("Continue doing something else.");Thread.sleep(1000);}return stringBuilder.toString();}
分析: 这些获取异步方法的结果信息,是通过不停的检查Future的状态来获取当前的异步方法是否执行完毕来实现的。
Spring默认线程池 SimpleAsyncTaskExecutor
Spring异步线程池的接口类是TaskExecutor,本质还是java.util.concurrent.Executor,没有配置的情况下,默认使用的是 SimpleAsyncTaskExecutor。
@Async演示Spring默认的SimpleAsyncTaskExecutor
@Component@EnableAsyncpublicclassScheduleTask{SimpleDateFormat sdf =newSimpleDateFormat("yyyy-MM-dd HH:mm:ss");@Async@Scheduled(fixedRate =2000)publicvoidtestScheduleTask(){try{Thread.sleep(6000);System.out.println("Spring1自带的线程池"+Thread.currentThread().getName()+"-"+ sdf.format(newDate()));}catch(InterruptedException e){
e.printStackTrace();}}@Async@Scheduled(cron ="*/2 * * * * ?")publicvoidtestAsyn(){try{Thread.sleep(1000);System.out.println("Spring2自带的线程池"+Thread.currentThread().getName()+"-"+ sdf.format(newDate()));}catch(Exception ex){
ex.printStackTrace();}}}
从运行结果可以看出Spring默认的@Async用线程池名字为SimpleAsyncTaskExecutor,而且每次都会重新创建一个新的线程,所以可以看到TaskExecutor-后面带的数字会一直变大。
SimpleAsyncTaskExecutor的特点是,每次执行任务时,它会重新启动一个新的线程,并允许开发者控制并发线程的最大数量(concurrencyLimit),从而起到一定的资源节流作用。默认是concurrencyLimit取值为-1,即不启用资源节流。
Spring的线程池 ThreadPoolTaskExecutor (自定义线程池)
上面介绍了Spring默认的线程池simpleAsyncTaskExecutor,但是Spring更加推荐我们开发者使用ThreadPoolTaskExecutor类来创建线程池,其本质是对java.util.concurrent.ThreadPoolExecutor的包装。
application.properties
# 核心线程池数
spring.task.execution.pool.core-size=5
# 最大线程池数
spring.task.execution.pool.max-size=10
# 任务队列的容量
spring.task.execution.pool.queue-capacity=5
# 非核心线程的存活时间
spring.task.execution.pool.keep-alive=60
# 线程池的前缀名称
spring.task.execution.thread-name-prefix=god-jiang-task-
AsyncScheduledTaskConfig.java
@ConfigurationpublicclassAsyncScheduledTaskConfig{@Value("${spring.task.execution.pool.core-size}")privateint corePoolSize;@Value("${spring.task.execution.pool.max-size}")privateint maxPoolSize;@Value("${spring.task.execution.pool.queue-capacity}")privateint queueCapacity;@Value("${spring.task.execution.thread-name-prefix}")privateString namePrefix;@Value("${spring.task.execution.pool.keep-alive}")privateint keepAliveSeconds;@BeanpublicExecutormyAsync(){ThreadPoolTaskExecutor executor =newThreadPoolTaskExecutor();//最大线程数
executor.setMaxPoolSize(maxPoolSize);//核心线程数
executor.setCorePoolSize(corePoolSize);//任务队列的大小
executor.setQueueCapacity(queueCapacity);//线程前缀名
executor.setThreadNamePrefix(namePrefix);//线程存活时间
executor.setKeepAliveSeconds(keepAliveSeconds);/**
* 拒绝处理策略
* CallerRunsPolicy():交由调用方线程运行,比如 main 线程。
* AbortPolicy():直接抛出异常。
* DiscardPolicy():直接丢弃。
* DiscardOldestPolicy():丢弃队列中最老的任务。
*/
executor.setRejectedExecutionHandler(newThreadPoolExecutor.AbortPolicy());//线程初始化
executor.initialize();return executor;}}
注意,这个方法的类一定要交给Spring容器来管理
@Component@EnableAsyncpublicclassScheduleTask{SimpleDateFormat sdf =newSimpleDateFormat("yyyy-MM-dd HH:mm:ss");@Async("myAsync")@Scheduled(fixedRate =2000)publicvoidtestScheduleTask(){try{Thread.sleep(6000);System.out.println("Spring1自带的线程池"+Thread.currentThread().getName()+"-"+ sdf.format(newDate()));}catch(InterruptedException e){
e.printStackTrace();}}@Async("myAsync")@Scheduled(cron ="*/2 * * * * ?")publicvoidtestAsyn(){try{Thread.sleep(1000);System.out.println("Spring2自带的线程池"+Thread.currentThread().getName()+"-"+ sdf.format(newDate()));}catch(Exception ex){
ex.printStackTrace();}}}
以上从运行结果可以看出,自定义ThreadPoolTaskExecutor可以实现线程的复用,而且还能控制好线程数,写出更好的多线程并发程序。
第二种自定义方式
第一种方式的线程池使用时候总要加上注解 @Async(“myAsync”),而这种方式是重写 spring 默认线程池的方式,使用的时候只需要加 @Async 注解就可以了,不用去声明线程池类。
NativeAsyncTaskExecutePool.java 装配线程池
*** 原生(Spring)异步任务线程池装配类,实现AsyncConfigurer重写他的两个方法,这样在使用默认的
* 线程池的时候就会使用自己重写的
*/@Slf4j@ConfigurationpublicclassNativeAsyncTaskExecutePoolimplementsAsyncConfigurer{//注入配置类@AutowiredTaskThreadPoolConfig config;@OverridepublicExecutorgetAsyncExecutor(){ThreadPoolTaskExecutor executor =newThreadPoolTaskExecutor();//核心线程池大小
executor.setCorePoolSize(config.getCorePoolSize());//最大线程数
executor.setMaxPoolSize(config.getMaxPoolSize());//队列容量
executor.setQueueCapacity(config.getQueueCapacity());//活跃时间
executor.setKeepAliveSeconds(config.getKeepAliveSeconds());//线程名字前缀
executor.setThreadNamePrefix("NativeAsyncTaskExecutePool-");// setRejectedExecutionHandler:当pool已经达到max size的时候,如何处理新任务// CallerRunsPolicy:不在新线程中执行任务,而是由调用者所在的线程来执行
executor.setRejectedExecutionHandler(newThreadPoolExecutor.CallerRunsPolicy());// 等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);// 可在这初始化,也可以不初始化,在调用的时候再初始化
executor.initialize();return executor;}/**
* 异步任务中异常处理
* @return
*/@OverridepublicAsyncUncaughtExceptionHandlergetAsyncUncaughtExceptionHandler(){returnnewAsyncUncaughtExceptionHandler(){@OverridepublicvoidhandleUncaughtException(Throwable arg0,Method arg1,Object... arg2){
log.error("=========================="+arg0.getMessage()+"=======================", arg0);
log.error("exception method:"+arg1.getName());}};}}
注意点
关于注解失效需要注意以下几点
- 注解的方法必须是public方法
- 方法一定要从另一个类中调用,也就是从类的外部调用,类的内部调用是无效的,因为@Transactional和@Async注解的实现都是基于Spring的AOP,而AOP的实现是基于动态代理模式实现的。那么注解失效的原因就很明显了,有可能因为调用方法的是对象本身而不是代理对象,因为没有经过Spring容器。
- 异步方法使用注解@Async的返回值只能为void或者Future
解决办法:
如果要使同一个类中的方法之间调用也被拦截,需要使用spring容器中的实例对象,而不是使用默认的this,因为通过bean实例的调用才会被spring的aop拦截
本例使用方法:AsyncService asyncService = context.getBean(AsyncService.class); 然后使用这个引用调用本地的方法即可达到被拦截的目的。
为什么要使用自定义线程池
如果不自定义异步方法的线程池默认使用SimpleAsyncTaskExecutor。SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,每次调用都会创建一个新的线程。并发大的时候会产生严重的性能问题。
Spring异步线程池接口 TaskExecutor
看源码可知
@FunctionalInterfacepublicinterfaceTaskExecutorextendsExecutor{voidexecute(Runnable var1);}
它的实先类有很多如下:
- SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,每次调用都会创建一个新的线程。
- SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方
- ConcurrentTaskExecutor:Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类
- SimpleThreadPoolTaskExecutor:是Quartz的SimpleThreadPool的类。线程池同时被quartz和非quartz使用,才需要使用此类
- ThreadPoolTaskExecutor :最常使用,推荐。 其实质是对java.util.concurrent.ThreadPoolExecutor的包装
拓展
内存溢出的三种类型
- 第一种OutOfMemoryError: PermGen space,发生这种问题的原因是程序中使用了大量的jar或class
- 第二种OutOfMemoryError: Java heap space,发生这种问题的原因是java虚拟机创建的对象太多
- 第三种OutOfMemoryError:unable to create new native thread,创建线程数量太多,占用内存过大
线程池拒绝策略
rejectedExectutionHandler参数字段用于配置绝策略,常用拒绝策略如下
- AbortPolicy:用于被拒绝任务的处理程序,它将抛出RejectedExecutionException
- CallerRunsPolicy:用于被拒绝任务的处理程序,它直接在execute方法的调用线程中运行被拒绝的任务。
- DiscardOldestPolicy:用于被拒绝任务的处理程序,它放弃最旧的未处理请求,然后重试execute。
- DiscardPolicy:用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。
版权归原作者 hundan_520520 所有, 如有侵权,请联系我们删除。