目录
1.简单介绍
2.springboot开启异步任务
3.实验验证
4.注意事项
异步任务:有时候在某个调用中,我们需要调用 A, B, C三个业务流程;如他们都是同步调用,则需要将他们都顺序执行完毕之后,方才算作过程执行完毕,需要A+B+C三个业务一共的执行时间;但如果就我们可以让A、B、C三个业务异步同时执行(前提是三个任务没有先后顺序关系),那么我们就只需要A、B、C三个业务中执行时间最长的那个任务的时间即可执行完毕。这就是异步执行任务。
在Spring中,基于@Async标注的方法,称之为异步方法;这些方法将在执行的时候,将会在独立的线程中被执行,调用者无需等待它的完成,即可继续其他的操作 。
@Async 的原理是通过 Spring AOP动态代理 的方式来实现的。Spring容器启动初始化bean时,判断类中是否使用了@Async 注解:如果使用了,则为其创建切入点和切入点处理器,根据切入点创建代理,在线程调用@Async注解标注的方法时,会调用代理,执行切入点处理器invoke方法,将方法的执行提交给线程池中的另外一个线程来处理,从而实现了异步执行。
首先,我们需要编写自己的线程池,避免spring自身不断地创建线程,导致内存溢出等问题。
自定义线程池的方法有两种,可自行进行选择:
(1)application.xml中配置:
Spring:
task:
execution:
pool:
max-size: 50 #最大线程数:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
core-size: 10 #心线程数:线程池创建时候初始化准备就绪的线程数
queue-capacity: 100 #线程池所使用的缓冲队列
keep-alive: 60 #允许线程的空闲时间:超过了核心线程出之外的线程,在空闲时间到达之后会被释放
此方式还需要在启动类中添加 @EnableAsync 注解,表示开启异步任务。
(2)编写配置类: (建议使用该方法)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
@EnableAsync // 启用异步任务
public class AsyncConfiguration implements AsyncConfigurer {
// 声明一个线程池(并指定线程池的名字)
@Bean("AsyncTask")
public ThreadPoolTaskExecutor asyncExecutor() {
//创建线程池
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//核心线程数:线程池创建时候初始化准备就绪的线程数,
//作用:等待来接受异步任务,一直存在不会被释放
int corePoolSize = 10;
executor.setCorePoolSize(corePoolSize);
//线程池所使用的缓冲队列
executor.setQueueCapacity(100);
//等待任务在关机时完成--表明等待所有线程执行完
executor.setWaitForTasksToCompleteOnShutdown(true);
//最大线程数:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
//作用:控制资源并发
executor.setMaxPoolSize(corePoolSize*5);
//允许线程的空闲时间:超过了核心线程出之外的线程,在空闲时间到达之后会被释放
executor.setKeepAliveSeconds(30);
//线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
executor.setThreadNamePrefix("Async-");
executor.initialize();
return executor;
}
}
其中 ThreadPoolTaskExecutor类是spring封装的线程池包,其使用方式与java juc包下的ThreadPoolExecutor 类相似。
而关于线程池中线程的分配,流程如下:
以上述代码中线程池为例(10 core,50 max,100 queue),当200个并发同时进来时,首先会占用10个核心线程,然后100个进入缓冲队列,缓冲队列满了后,根据max 再打开新线程40个,剩下50个则会根据丢弃策略进行丢弃。
然后,我们只需要在对应的业务类中的方法是标记 @Async注解即可开启异步任务:
@Override
@Async("AsyncTask")
public void testTask01() {
log.info("任务1开启");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("任务1结束");
}
关于@Async注解注解:
@Async("AsyncTask")
public Future testTask03() {
log.info("任务3开启");
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("任务3结束");
return new AsyncResult<>("success");
}
然后,我们可以通过 get 方法获取值"success"。
业务类中添加任务2代码:
@Override
@Async("AsyncTask")
public void testTask02() {
log.info("任务2开启");
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("任务2结束");
}
在控制类中编写如下代码,进行验证:
@GetMapping("/test")
public void test(){
long start = System.currentTimeMillis();
userAddressService.testTask01();
userAddressService.testTask02();
long end = System.currentTimeMillis();
log.info("future(): 耗时" + (end - start) + "ms");
}
当任务1和任务2不开启异步任务时,顺序执行,效果如下:
而当任务1和任务2开启异步任务后,效果如下:
可以看到并没有顺序执行,遇到异步任务后新开了一个线程执行,然后直接执行main线程中的代码;任务1和任务2则是同时开始执行,成功异步执行。
值得注意的是,如果controller的方法中设置了返回值,且返回值和异步方法的返回值无关(或无返回值)执行时会不等异步方法执行完,先返回结果至前端。
修改控制类中代码,改为循环执行任务1:
@GetMapping("/test")
public void test(){
long start = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
userAddressService.testTask01();
}
//userAddressService.testTask02();
long end = System.currentTimeMillis();
log.info("future(): 耗时" + (end - start) + "ms");
}
此处需要注意的是同时并发的任务数不要超过缓冲队列数+最大线程数,否则在分配完线程资源后会抛出异常。具体如何设置线程池,还需要根据实际情况考虑。
如下方式会使@Async注解失效