目录
异步任务
1.1 什么叫异步
1、Java线程处理
2、SpringBoot异步任务
2.1 使用注解@EnableAsync开启异步任务支持
2.2、使用@Async注解标记要进行异步执行的方法
2.3、controller测试
3、异步任务相关限制
4、自定义 Executor(自定义线程池)
4.1、应用层级:
4.2、方法层级:
有时候,前端可能提交了一个耗时任务,如果后端接收到请求后,直接执行该耗时任务,那么前端需要等待很久一段时间才能接受到响应。如果该耗时任务是通过浏览器直接进行请求,那么浏览器页面会一直处于转圈等待状态。
事实上,当后端要处理一个耗时任务时,通常都会将耗时任务提交到一个异步任务中进行执行,此时前端提交耗时任务后,就可直接返回,进行其他操作。
异步:如果方法中有休眠任务,不用等任务执行完,直接执行下一个任务
简单来说:客户端发送请求,可以跳过方法,执行下一个方法,
如果其中一个A方法有休眠任务,不需要等待,直接执行下一个方法,异步任务(A方法)会在后台得到执行,等A方法的休眠时间到了再去执行A方法
同步:一定要等任务执行完了,得到结果,才执行下一个任务。。
而异步比如:
setTimeout(function cbFn(){ console.log('learnInPro'); }, 1000); console.log('sync things');
setTimeout就是一个异步任务,当JS引擎顺序执行到setTimeout的时候发现他是个异步任务,则会把这个任务挂起,继续执行后面的代码。直到1000ms后,回调函数cbFn才会执行,这就是异步,在执行到setTimeout的时候,JS并不会傻呵呵的等着1000ms执行cbFn回调函数,而是继续执行了后面的代码
概念:所谓"异步",简单说就是一个任务不是连续完成的,可以理解成该任务被人为分成两段,先执行第一段,然后转而执行其他任务,
相应地,连续的执行就叫做同步。由于是连续执行,不能插入其他任务,所以操作系统从硬盘读取文件的这段时间,程序只能干等着。
在 Java 中,开启异步任务最常用的方式就是开辟线程执行异步任务,如下所示:
@RestController @RequestMapping("async") public class AsyncController { @GetMapping("/") public String index() { new Thread(new Runnable() { @Override public void run() { try { // 模拟耗时操作 Thread.sleep(TimeUnit.SECONDS.toMillis(5)); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); return "consuming time behavior processing!"; } }
这时浏览器请求localhost:8080/async/,就可以很快得到响应,并且耗时任务会在后台得到执行。
一般来说,前端不会关注耗时任务结果,因此前端只需负责提交该任务给到后端即可。但是如果前端需要获取耗时任务结果,则可通过Future等方式将结果返回,详细内容如下
public class MyReturnableTask implements Callable{ @Override public String call() throws Exception { long startTime = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName()+"线程运行开始"); Thread.sleep(5000); System.out.println(Thread.currentThread().getName()+"线程运行结束"); return "result"; } }
@GetMapping("/task") public void task() throws ExecutionException, InterruptedException { MyReturnableTask myReturnableTask = new MyReturnableTask(); FutureTaskfutureTask = new FutureTask (myReturnableTask); Thread thread = new Thread(futureTask, "returnableThread"); thread.start(); String s = futureTask.get(); System.out.println(s); }
事实上,在 Spring Boot 中,我们不需要手动创建线程异步执行耗时任务,因为 Spring 框架已提供了相关异步任务执行解决方案,本文主要介绍下在 Spring Boot 中执行异步任务的相关内容。
@SpringBootApplication @EnableAsync//开启异步任务支持 public class ApplicationStarter { public static void main(String[] args) { SpringApplication.run(ApplicationStarter.class,args); } }
public interface AsyncService { //异步任务 void t1(); Futuret2(); }
@Service public class AsyncServiceImpl implements AsyncService { //使用@Async注解标记的方法 会提交到一个异步任务中进行执行,第一次不会执行该方法, //如果不添加该注解,controller中调用该方法会等待5秒在响应 //异步任务 @Async public void t1() { // 模拟耗时任务 try { Thread.sleep(TimeUnit.SECONDS.toMillis(5)); } catch (InterruptedException e) { e.printStackTrace(); } //因为该异步方法中使用了休眠,所以过5秒才会执行下面代码 System.out.println("异步方法中:耗时时间已走完"); } @Async public Futuret2(){ // 模拟耗时任务 try { Thread.sleep(TimeUnit.SECONDS.toMillis(5)); } catch (InterruptedException e) { e.printStackTrace(); } return new AsyncResult<>("async tasks done!"); } }
@RestController public class AsyncController { @Autowired private AsyncService asyncService; @GetMapping("/task1") public String asyncTaskWithoutReturnType(){ //因为下面方法是异步任务方法,前端请求过来,会先跳过异步方法(异步任务会在后台得到执行),接着会执行下面代码。 //因为异步任务中有耗时任务,在因为异步任务会在后台得到执行,所以等待时间耗完,就会在执行异步方法中的内容(这相当于回调了) asyncService.t1(); return "rrrr"; } @GetMapping("/task2") public String asyncTaskWithReturnType() throws InterruptedException, ExecutionException { asyncService.t2(); return "aaa"; } }
被@Async注解的方法可以接受任意类型参数,但只能返回void或Future类型数据
所以当异步方法返回数据时,需要使用Future包装异步任务结果,上述代码使用AsyncResult包装异步任务结果,AsyncResult间接继承Future,是 Spring 提供的一个可用于追踪异步方法执行结果的包装类。其他常用的Future类型还有 Spring 4.2 提供的ListenableFuture,或者 JDK 8 提供的CompletableFuture,这些类型可提供更丰富的异步任务操作。
如果前端需要获取耗时任务结果,则异步任务方法应当返回一个Future类型数据,此时Controller相关接口需要调用该Future的get()方法获取异步任务结果,get()方法是一个阻塞方法,因此该操作相当于将异步任务转换为同步任务,浏览器同样会面临我们前面所讲的转圈等待过程,但是异步执行还是有他的好处的,因为我们可以控制get()方法的调用时序,因此可以先执行其他一些操作后,最后再调用get()方法
被@Async注解的异步任务方法存在相关限制:
被@Async注解的方法必须是public的,这样方法才可以被代理。
不能在同一个类中调用@Async方法,因为同一个类中调用会绕过方法代理,调用的是实际的方法。
被@Async注解的方法不能是static。
@Async注解不能与 Bean 对象的生命周期回调函数(比如@PostConstruct)一起注解到同一个方法中。解决方法可参考:Spring - The @Async annotation
异步类必须注入到 Spring IOC 容器中(也即异步类必须被@Component/@Service等进行注解)。
其他类中使用异步类对象必须通过@Autowired等方式进行注入,不能手动new对象。
默认情况下,Spring 会自动搜索相关线程池定义:要么是一个唯一TaskExecutor Bean 实例,要么是一个名称为taskExecutor的Executor Bean 实例。如果这两个 Bean 实例都不存在,就会使用SimpleAsyncTaskExecutor来异步执行被@Async注解的方法。
综上,可以知道,默认情况下,Spring 使用的 Executor 是SimpleAsyncTaskExecutor,SimpleAsyncTaskExecutor每次调用都会创建一个新的线程,不会重用之前的线程。很多时候,这种实现方式不符合我们的业务场景,因此通常我们都会自定义一个 Executor 来替换SimpleAsyncTaskExecutor。
对于自定义 Executor(自定义线程池),可以分为如下两个层级:
应用层级:即全局生效的 Executor。依据 Spring 默认搜索机制,其实就是配置一个全局唯一的TaskExecutor实例或者一个名称为taskExecutor的Executor实例即可,如下所示:
方法层级:即为单独一个或多个方法指定运行线程池,其他未指定的异步方法运行在默认线程池。如下所示:
下面代码定义了一个名称为taskExecutor的Executor,此时@Async方法默认就会运行在该Executor中。
@Configuration public class ExcuterConfig { @Bean("taskExecutor") public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // 设置核心线程数 int cores = Runtime.getRuntime().availableProcessors(); executor.setCorePoolSize(cores); // 设置最大线程数 executor.setMaxPoolSize(20); // 等待所有任务结束后再关闭线程池 executor.setWaitForTasksToCompleteOnShutdown(true); // 设置线程默认前缀名 executor.setThreadNamePrefix("Application-Level-Async-"); return executor; } }
package com.buba.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.task.TaskExecutor; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; /** * @author qlx */ @Configuration public class ExcuterConfig { @Bean("methodLevelExecutor1") public TaskExecutor getAsyncExecutor1() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // 设置核心线程数 executor.setCorePoolSize(4); // 设置最大线程数 executor.setMaxPoolSize(20); // 等待所有任务结束后再关闭线程池 executor.setWaitForTasksToCompleteOnShutdown(true); // 设置线程默认前缀名 executor.setThreadNamePrefix("Method-Level-Async1-"); return executor; } @Bean("methodLevelExecutor2") public TaskExecutor getAsyncExecutor2() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // 设置核心线程数 executor.setCorePoolSize(8); // 设置最大线程数 executor.setMaxPoolSize(20); // 等待所有任务结束后再关闭线程池 executor.setWaitForTasksToCompleteOnShutdown(true); // 设置线程默认前缀名 executor.setThreadNamePrefix("Method-Level-Async2-"); return executor; } }
上述特意设置了多个TaskExecutor,因为如果只设置一个TaskExecutor,那么 Spring 就会默认采用该TaskExecutor作为所有@Async的Executor,而设置了多个TaskExecutor,Spring 检测到全局存在多个Executor,就会降级使用默认的SimpleAsyncTaskExecutor,此时我们就可以为@Async方法配置执行线程池,其他未配置的@Async就会默认运行在SimpleAsyncTaskExecutor中,这就是方法层级的自定义 Executor。如下代码所示:
@Service public class AsyncService { @Async("methodLevelExecutor1") public void t1() throws InterruptedException { // 模拟耗时任务 Thread.sleep(TimeUnit.SECONDS.toMillis(5)); } @Async("methodLevelExecutor2") public Futuret2() throws InterruptedException { // 模拟耗时任务 Thread.sleep(TimeUnit.SECONDS.toMillis(5)); return new AsyncResult<>("async tasks done!"); } }