面试|SpringBoot 异步使用@Async原理及线程池配置

所谓异步任务,其实就是异步执行程序,有些时候遇到一些耗时的的任务,如果一直卡等待,肯定会影响其他程序的执行,所以就让这些程序需要以异步的方式去执行。那么下面就来介绍Spring Boot 如何实现异步任务。
Spring中用@Async注解标记的方法,称为异步方法。在spring boot应用中使用@Async很简单:

  1. 调用异步方法类上或者启动类加上注解@EnableAsync
  2. 在需要被异步调用的方法外加上@Async
  3. 所使用的@Async注解方法的类对象应该是Spring容器管理的bean对象
注解配置开启 在springboot启动类(或是配置类)上添加 @EnableAsync 注解
@EnableAsync public class SpringBootApplication {}

基于@Async无返回值调用
/** * 没有返回值的Async方法 */ @Async public void asyncMethodWithVoidReturnType() { log.info("没有返回值的Async方法, ThreadName : {}", Thread.currentThread().getName()); }

基于@Async返回值的调用
/** * 有返回值的Async方法 * @returnFuturenew AsyncResult */ @Override @Async public Future asyncMethodWithReturnType() { log.info("有返回值的Async方法, ThreadName : {}", Thread.currentThread().getName()); try { Thread.sleep(5000); return new AsyncResult("hello world !!!!"); } catch (InterruptedException e) { log.error("出问题了, {}", e.getMessage()); } return null; }

以上示例可以发现,返回的数据类型为Future类型,其为一个接口。具体的结果类型为 AsyncResult,这个是需要注意的地方。
调用返回结果的异步方法示例:
@GetMapping("/future") public String futureAsync() throws ExecutionException, InterruptedException { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("Invoking an asynchronous method. threadName : " + Thread.currentThread().getName()); Future 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 @EnableAsync public class ScheduleTask { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @Async @Scheduled(fixedRate = 2000) public void testScheduleTask() { try { Thread.sleep(6000); System.out.println("Spring1自带的线程池" + Thread.currentThread().getName() + "-" + sdf.format(new Date())); } catch (InterruptedException e) { e.printStackTrace(); } } @Async @Scheduled(cron = "*/2 * * * * ?") public void testAsyn() { try { Thread.sleep(1000); System.out.println("Spring2自带的线程池" + Thread.currentThread().getName() + "-" + sdf.format(new Date())); } catch (Exception ex) { ex.printStackTrace(); } } }

面试|SpringBoot 异步使用@Async原理及线程池配置
文章图片

从运行结果可以看出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
@Configuration public class AsyncScheduledTaskConfig { @Value("${spring.task.execution.pool.core-size}") private int corePoolSize; @Value("${spring.task.execution.pool.max-size}") private int maxPoolSize; @Value("${spring.task.execution.pool.queue-capacity}") private int queueCapacity; @Value("${spring.task.execution.thread-name-prefix}") private String namePrefix; @Value("${spring.task.execution.pool.keep-alive}") private int keepAliveSeconds; @Bean public Executor myAsync() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); //最大线程数 executor.setMaxPoolSize(maxPoolSize); //核心线程数 executor.setCorePoolSize(corePoolSize); //任务队列的大小 executor.setQueueCapacity(queueCapacity); //线程前缀名 executor.setThreadNamePrefix(namePrefix); //线程存活时间 executor.setKeepAliveSeconds(keepAliveSeconds); /** * 拒绝处理策略 * CallerRunsPolicy():交由调用方线程运行,比如 main 线程。 * AbortPolicy():直接抛出异常。 * DiscardPolicy():直接丢弃。 * DiscardOldestPolicy():丢弃队列中最老的任务。 */ executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); //线程初始化 executor.initialize(); return executor; } }

注意,这个方法的类一定要交给Spring容器来管理
@Component @EnableAsync public class ScheduleTask { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @Async("myAsync") @Scheduled(fixedRate = 2000) public void testScheduleTask() { try { Thread.sleep(6000); System.out.println("Spring1自带的线程池" + Thread.currentThread().getName() + "-" + sdf.format(new Date())); } catch (InterruptedException e) { e.printStackTrace(); } } @Async("myAsync") @Scheduled(cron = "*/2 * * * * ?") public void testAsyn() { try { Thread.sleep(1000); System.out.println("Spring2自带的线程池" + Thread.currentThread().getName() + "-" + sdf.format(new Date())); } catch (Exception ex) { ex.printStackTrace(); } } }

以上从运行结果可以看出,自定义ThreadPoolTaskExecutor可以实现线程的复用,而且还能控制好线程数,写出更好的多线程并发程序。
第二种自定义方式
第一种方式的线程池使用时候总要加上注解 @Async(“myAsync”),而这种方式是重写 spring 默认线程池的方式,使用的时候只需要加 @Async 注解就可以了,不用去声明线程池类。
NativeAsyncTaskExecutePool.java 装配线程池
** * 原生(Spring)异步任务线程池装配类,实现AsyncConfigurer重写他的两个方法,这样在使用默认的 *线程池的时候就会使用自己重写的 */ @Slf4j @Configuration public class NativeAsyncTaskExecutePool implements AsyncConfigurer{//注入配置类 @Autowired TaskThreadPoolConfig config; @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); //核心线程池大小 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(new ThreadPoolExecutor.CallerRunsPolicy()); // 等待所有任务结束后再关闭线程池 executor.setWaitForTasksToCompleteOnShutdown(true); // 可在这初始化,也可以不初始化,在调用的时候再初始化 executor.initialize(); return executor; }/** *异步任务中异常处理 * @return */ @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return new AsyncUncaughtExceptionHandler() { @Override public void handleUncaughtException(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
看源码可知
@FunctionalInterface public interface TaskExecutor extends Executor { void execute(Runnable var1); }

它的实先类有很多如下:
面试|SpringBoot 异步使用@Async原理及线程池配置
文章图片

  1. SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,每次调用都会创建一个新的线程。
  2. SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方
  3. ConcurrentTaskExecutor:Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类
  4. SimpleThreadPoolTaskExecutor:是Quartz的SimpleThreadPool的类。线程池同时被quartz和非quartz使用,才需要使用此类
  5. 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:用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。
【面试|SpringBoot 异步使用@Async原理及线程池配置】先自我介绍一下,小编13年上师交大毕业,曾经在小公司待过,去过华为OPPO等大厂,18年进入阿里,直到现在。深知大多数初中级java工程师,想要升技能,往往是需要自己摸索成长或是报班学习,但对于培训机构动则近万元的学费,着实压力不小。自己不成体系的自学效率很低又漫长,而且容易碰到天花板技术停止不前。因此我收集了一份《java开发全套学习资料》送给大家,初衷也很简单,就是希望帮助到想自学又不知道该从何学起的朋友,同时减轻大家的负担。添加下方名片,即可获取全套学习资料哦

    推荐阅读