本篇文章咱们以SpringBoot中异步的使用(包括:异步调用和异步方法两个维度)来进行讲解。css
咱们先经过一张图来区分一下异步请求和同步请求的区别:前端
在上图中有三个角色:客户端、Web容器和业务处理线程。java
两个流程中客户端对Web容器的请求,都是同步的。由于它们在请求客户端时都处于阻塞等待状态,并无进行异步处理。web
在Web容器部分,第一个流程采用同步请求,第二个流程采用异步回调的形式。spring
经过异步处理,能够先释放容器分配给请求的线程与相关资源,减轻系统负担,从而增长了服务器对客户端请求的吞吐量。但并发请求量较大时,一般会经过负载均衡的方案来解决,而不是异步。typescript
Servlet 3.0以前,Servlet采用Thread-Per-Request的方式处理请求,即每一次Http请求都由一个线程从头至尾处理。当涉及到耗时操做时,性能问题便比较明显。浏览器
Servlet 3.0中提供了异步处理请求。能够先释放容器分配给请求的线程与相关资源,减轻系统负担,从而增长服务的吞吐量。服务器
Servlet 3.0的异步是经过AsyncContext对象来完成的,它能够从当前线程传给另外一个线程,并归还初始线程。新的线程处理完业务能够直接返回结果给客户端。网络
AsyncContext对象能够从HttpServletRequest中获取:并发
@RequestMapping("/async")public void async(HttpServletRequest request) { AsyncContext asyncContext = request.getAsyncContext(); }
在AsyncContext中提供了获取ServletRequest、ServletResponse和添加监听(addListener)等功能:
public interface AsyncContext { ServletRequest getRequest(); ServletResponse getResponse(); void addListener(AsyncListener var1); void setTimeout(long var1); // 省略其余方法}
不只能够经过AsyncContext获取Request和Response等信息,还能够设置异步处理超时时间。一般,超时时间(单位毫秒)是须要设置的,否则无限等下去不就与同步处理同样了。
经过AsyncContext的addListener还能够添加监听事件,用来处理异步线程的开始、完成、异常、超时等事件回调。
addListener方法的参数AsyncListener的源码以下:
public interface AsyncListener extends EventListener { // 异步执行完毕时调用 void onComplete(AsyncEvent var1) throws IOException; // 异步线程执行超时调用 void onTimeout(AsyncEvent var1) throws IOException; // 异步线程出错时调用 void onError(AsyncEvent var1) throws IOException; // 异步线程开始时调用 void onStartAsync(AsyncEvent var1) throws IOException; }
一般,异常或超时时返回调用方错误信息,而异常时会处理一些清理和关闭操做或记录异常日志等。
下面直接看一个基于Servlet方式的异步请求示例:
@GetMapping(value = "/email/send")public void servletReq(HttpServletRequest request) { AsyncContext asyncContext = request.startAsync(); // 设置监听器:可设置其开始、完成、异常、超时等事件的回调处理 asyncContext.addListener(new AsyncListener() { @Override public void onTimeout(AsyncEvent event) { System.out.println("处理超时了..."); } @Override public void onStartAsync(AsyncEvent event) { System.out.println("线程开始执行"); } @Override public void onError(AsyncEvent event) { System.out.println("执行过程当中发生错误:" + event.getThrowable().getMessage()); } @Override public void onComplete(AsyncEvent event) { System.out.println("执行完成,释放资源"); } }); //设置超时时间 asyncContext.setTimeout(6000); asyncContext.start(new Runnable() { @Override public void run() { try { Thread.sleep(5000); System.out.println("内部线程:" + Thread.currentThread().getName()); asyncContext.getResponse().getWriter().println("async processing"); } catch (Exception e) { System.out.println("异步处理发生异常:" + e.getMessage()); } // 异步请求完成通知,整个请求完成 asyncContext.complete(); } }); //此时request的线程链接已经释放了 System.out.println("主线程:" + Thread.currentThread().getName()); }
启动项目,访问对应的URL,打印日志以下:
主线程:http-nio-8080-exec-4内部线程:http-nio-8080-exec-5执行完成,释放资源
能够看出,上述代码先执行完了主线程,也就是程序的最后一行代码的日志打印,而后才是内部线程的执行。内部线程执行完成,AsyncContext的onComplete方法被调用。
若是经过浏览器访问对应的URL,还能够看到该方法的返回值“async processing”。说明内部线程的结果一样正常的返回到客户端了。
基于Spring能够经过Callable、DeferredResult或者WebAsyncTask等方式实现异步请求。
对于一次请求(/email),基于Callable的处理流程以下:
一、Spring MVC开启副线程处理业务(将Callable提交到TaskExecutor);
二、DispatcherServlet和全部的Filter退出Web容器的线程,可是response保持打开状态;
三、Callable返回结果,SpringMVC将原始请求从新派发给容器(再从新请求一次/email),恢复以前的处理;
四、DispatcherServlet从新被调用,将结果返回给用户;
代码实现示例以下:
@GetMapping("/email")public Callable<String> order() { System.out.println("主线程开始:" + Thread.currentThread().getName()); Callable<String> result = () -> { System.out.println("副线程开始:" + Thread.currentThread().getName()); Thread.sleep(1000); System.out.println("副线程返回:" + Thread.currentThread().getName()); return "success"; }; System.out.println("主线程返回:" + Thread.currentThread().getName()); return result; }
访问对应URL,控制台输入日志以下:
主线程开始:http-nio-8080-exec-1主线程返回:http-nio-8080-exec-1副线程开始:task-1副线程返回:task-1
经过日志能够看出,主线程已经完成了,副线程才进行执行。同时,URL返回结果“success”。这也说明一个问题,服务器端的异步处理对客户端来讲是不可见的。
Callable默认使用SimpleAsyncTaskExecutor类来执行,这个类很是简单并且没有重用线程。在实践中,须要使用AsyncTaskExecutor类来对线程进行配置。
这里经过实现WebMvcConfigurer接口来完成线程池的配置。
@Configurationpublic class WebConfig implements WebMvcConfigurer { @Resource private ThreadPoolTaskExecutor myThreadPoolTaskExecutor; /** * 配置线程池 */ @Bean(name = "asyncPoolTaskExecutor") public ThreadPoolTaskExecutor getAsyncThreadPoolTaskExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setCorePoolSize(2); taskExecutor.setMaxPoolSize(10); taskExecutor.setQueueCapacity(25); taskExecutor.setKeepAliveSeconds(200); taskExecutor.setThreadNamePrefix("thread-pool-"); // 线程池对拒绝任务(无线程可用)的处理策略,目前只支持AbortPolicy、CallerRunsPolicy;默认为后者 taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); taskExecutor.initialize(); return taskExecutor; } @Override public void configureAsyncSupport(final AsyncSupportConfigurer configurer) { // 处理callable超时 configurer.setDefaultTimeout(60 * 1000); configurer.setTaskExecutor(myThreadPoolTaskExecutor); configurer.registerCallableInterceptors(timeoutCallableProcessingInterceptor()); } @Bean public TimeoutCallableProcessingInterceptor timeoutCallableProcessingInterceptor() { return new TimeoutCallableProcessingInterceptor(); } }
为了验证打印的线程,咱们将实例代码中的System.out.println替换成日志输出,会发如今使用线程池以前,打印日志以下:
2021-02-21 09:45:37.144 INFO 8312 --- [nio-8080-exec-1] c.s.learn.controller.AsynController : 主线程开始:http-nio-8080-exec-12021-02-21 09:45:37.144 INFO 8312 --- [nio-8080-exec-1] c.s.learn.controller.AsynController : 主线程返回:http-nio-8080-exec-12021-02-21 09:45:37.148 INFO 8312 --- [ task-1] c.s.learn.controller.AsynController : 副线程开始:task-12021-02-21 09:45:38.153 INFO 8312 --- [ task-1] c.s.learn.controller.AsynController : 副线程返回:task-1
线程名称为“task-1”。让线程池生效以后,打印日志以下:
2021-02-21 09:50:28.950 INFO 8339 --- [nio-8080-exec-1] c.s.learn.controller.AsynController : 主线程开始:http-nio-8080-exec-12021-02-21 09:50:28.951 INFO 8339 --- [nio-8080-exec-1] c.s.learn.controller.AsynController : 主线程返回:http-nio-8080-exec-12021-02-21 09:50:28.955 INFO 8339 --- [ thread-pool-1] c.s.learn.controller.AsynController : 副线程开始:thread-pool-12021-02-21 09:50:29.956 INFO 8339 --- [ thread-pool-1] c.s.learn.controller.AsynController : 副线程返回:thread-pool-1
线程名称为“thread-pool-1”,其中前面的“thread-pool”正是咱们配置的线程池前缀。
除了线程池的配置,还能够配置统一异常处理,这里就再也不演示了。
Spring提供的WebAsyncTask是对Callable的包装,提供了更强大的功能,好比:处理超时回调、错误回调、完成回调等。
@GetMapping("/webAsyncTask")public WebAsyncTask<String> webAsyncTask() { log.info("外部线程:" + Thread.currentThread().getName()); WebAsyncTask<String> result = new WebAsyncTask<>(60 * 1000L, new Callable<String>() { @Override public String call() { log.info("内部线程:" + Thread.currentThread().getName()); return "success"; } }); result.onTimeout(new Callable<String>() { @Override public String call() { log.info("timeout callback"); return "timeout callback"; } }); result.onCompletion(new Runnable() { @Override public void run() { log.info("finish callback"); } }); return result; }
访问对应请求,打印日志:
2021-02-21 10:22:33.028 INFO 8547 --- [nio-8080-exec-1] c.s.learn.controller.AsynController : 外部线程:http-nio-8080-exec-12021-02-21 10:22:33.033 INFO 8547 --- [ thread-pool-1] c.s.learn.controller.AsynController : 内部线程:thread-pool-12021-02-21 10:22:33.055 INFO 8547 --- [nio-8080-exec-2] c.s.learn.controller.AsynController : finish callback
DeferredResult使用方式与Callable相似,但在返回结果时不同,它返回的时实际结果可能没有生成,实际的结果可能会在另外的线程里面设置到DeferredResult中去。
DeferredResult的这个特性对实现服务端推技术、订单过时时间处理、长轮询、模拟MQ的功能等高级应用很是重要。
关于DeferredResult的使用先来看一下官方的例子和说明:
@RequestMapping("/quotes")@ResponseBodypublic DeferredResult<String> quotes() { DeferredResult<String> deferredResult = new DeferredResult<String>(); // Save the deferredResult in in-memory queue ... return deferredResult; }// In some other thread...deferredResult.setResult(data);
上述示例中咱们能够发现DeferredResult的调用并不必定在Spring MVC当中,它能够是别的线程。官方的解释也是如此:
In this case the return value will also be produced from a separate thread. However, that thread is not known to Spring MVC. For example the result may be produced in response to some external event such as a JMS message, a scheduled task, etc.
也就是说,DeferredResult返回的结果也多是由MQ、定时任务或其余线程触发。来个实例:
@Controller@RequestMapping("/async/controller")public class AsyncHelloController { private List<DeferredResult<String>> deferredResultList = new ArrayList<>(); @ResponseBody @GetMapping("/hello") public DeferredResult<String> helloGet() throws Exception { DeferredResult<String> deferredResult = new DeferredResult<>(); //先存起来,等待触发 deferredResultList.add(deferredResult); return deferredResult; } @ResponseBody @GetMapping("/setHelloToAll") public void helloSet() throws Exception { // 让全部hold住的请求给与响应 deferredResultList.forEach(d -> d.setResult("say hello to all")); } }
第一个请求/hello,会先将deferredResult存起来,前端页面是一直等待(转圈)状态。直到发第二个请求:setHelloToAll,全部的相关页面才会有响应。
整个执行流程以下:
经过上述流程能够发现:利用DeferredResult可实现一些长链接的功能,好比当某个操做是异步时,能够先保存对应的DeferredResult对象,当异步通知回来时,再找到这个DeferredResult对象,在setResult处理结果便可。从而提升性能。
在SpringBoot中将一个方法声明为异步方法很是简单,只需两个注解便可@EnableAsync和@Async。其中@EnableAsync用于开启SpringBoot支持异步的功能,用在SpringBoot的启动类上。@Async用于方法上,标记该方法为异步处理方法。
须要注意的是@Async并不支持用于被@Configuration注解的类的方法上。同一个类中,一个方法调用另一个有@Async的方法,注解也是不会生效的。
@EnableAsync的使用示例:
@SpringBootApplication@EnableAsyncpublic class App { public static void main(String[] args) { SpringApplication.run(App.class, args); } }
@Async的使用示例:
@Servicepublic class SyncService { @Async public void asyncEvent() { // 业务处理 } }
@Async注解的使用与Callable有相似之处,在默认状况下使用的都是SimpleAsyncTaskExecutor线程池,可参考Callable中的方式来自定义线程池。
下面经过一个实例来验证一下,启动类上使用@EnableAsync,而后定义Controller类:
@RestControllerpublic class IndexController { @Resource private UserService userService; @RequestMapping("/async") public String async(){ System.out.println("--IndexController--1"); userService.sendSms(); System.out.println("--IndexController--4"); return "success"; } }
定义Service及异步方法:
@Servicepublic class UserService { @Async public void sendSms(){ System.out.println("--sendSms--2"); IntStream.range(0, 5).forEach(d -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println("--sendSms--3"); } }
若是先注释掉@EnableAsync和@Async注解,即正常状况下的业务请求,打印日志为:
--IndexController--1--sendSms--2--sendSms--3--IndexController--4
使用@EnableAsync和@Async注解时,打印日志以下:
--IndexController--1--IndexController--4--sendSms--2--sendSms--3
经过日志的对比咱们能够看出,使用了@Async的方法,会被当成一个子线程。因此,整个sendSms方法会在主线程执行完了以后执行。
这样的效果是否是跟咱们上面使用的其余形式的异步殊途同归?因此在文章最开始已经说到,网络上所谓的“异步调用与异步请求的区别”是并不存储在的,本质上都是一回事,只不过实现形式不一样而已。这里所提到异步方法,也就是将方法进行异步处理而已。
所在的包不一样:
经过所在的包,咱们应该隐隐约约感到一些区别,好比@Async是位于scheduling包中,而WebAsyncTask和DeferredResult是用于Web(Spring MVC)的,而Callable是用于concurrent(并发)处理的。
对于Callable,一般用于Controller方法的异步请求,固然也能够用于替换Runable的方式。在方法的返回上与正常的方法有所区别:
// 普通方法public String aMethod(){ }// 对照Callable方法public Callable<String> aMethod(){ }
而WebAsyncTask是对Callable的封装,提供了一些事件回调的处理,本质上区别不大。
DeferredResult使用方式与Callable相似,重点在于跨线程之间的通讯。
@Async也是替换Runable的一种方式,能够代替咱们本身建立线程。并且适用的范围更广,并不局限于Controller层,而能够是任何层的方法上。
固然,你们也能够从返回结果,异常处理等角度来分析一下,这里就再也不展开了。
通过上述的一步步分析,你们应该对于Servlet3.0及Spring中对异步处理有所了解。当了解了这些基础理论,实战实例,使用方法及注意事项以后,想必更可以对网络上的相关知识可以进一步的去伪存真。