所谓异步任务,其实就是异步执行程序,有些时候遇到一些耗时的的任务,如果一直卡等待,肯定会影响其他程序的执行,所以就让这些程序需要以异步的方式去执行。那么下面就来介绍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:用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。

————————————————
版权声明:本文为CSDN博主「我家有只小熊二」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/hundan_520520/article/details/124690284