所谓异步任务,其实就是异步执行程序,有些时候遇到一些耗时的的任务,如果一直卡等待,肯定会影响其他程序的执行,所以就让这些程序需要以异步的方式去执行。那么下面就来介绍Spring Boot 如何实现异步任务。
Spring中用@Async注解标记的方法,称为异步方法。在spring boot应用中使用@Async很简单:
- 调用异步方法类上或者启动类加上注解@EnableAsync
- 在需要被异步调用的方法外加上@Async
- 所使用的@Async注解方法的类对象应该是Spring容器管理的bean对象
注解配置开启
在springboot启动类(或是配置类)上添加 @EnableAsync 注解
@EnableAsync
public class SpringBootApplication {
}
基于@Async无返回值调用
/**
* 没有返回值的Async方法
*/
@Async
public void asyncMethodWithVoidReturnType() {
log.info("没有返回值的Async方法, ThreadName : {}", Thread.currentThread().getName());
}
基于@Async返回值的调用
/**
* 有返回值的Async方法
* @return Future new AsyncResult
*/
@Override
@Async
public Future<String> asyncMethodWithReturnType() {
log.info("有返回值的Async方法, ThreadName : {}", Thread.currentThread().getName());
try {
Thread.sleep(5000);
return new AsyncResult<String>("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<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
@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();
}
}
}
从运行结果可以看出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);
}
它的实先类有很多如下:
- 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:用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。