[springboot] 异步开发之异步调用

何为异步调用

异步调用前,我们说说它对应的同步调用。通常开发过程中,一般上我们都是同步调用,即:程序按定义的顺序依次执行的过程,每一行代码执行过程必须等待上一行代码执行完毕后才执行。而异步调用指:程序在执行时,无需等待执行的返回值可继续执行后面的代码。显而易见,同步有依赖相关性,而异步没有,所以异步可并发执行,可提高执行效率,在相同的时间做更多的事情。

题外话:除了异步同步外,还有一个叫回调。其主要是解决异步方法执行结果的处理方法,比如在希望异步调用结束时返回执行结果,这个时候就可以考虑使用回调机制。

Async异步调用

SpringBoot中使用异步调用是很简单的,只需要使用@Async注解即可实现方法的异步调用。

注意:需要在启动类或配置类加入@EnableAsync使异步调用@Async注解生效。

@Configuration
@EnableAsync
public class AsyncConfig ...

使用@Async很简单,只需要在需要异步执行的方法上加入此注解即可。这里创建一个控制层和一个服务层,进行简单示例下。

@Slf4j
@Service
public class AsyncService {

	@Async
	public void asyncEvent() {
		log.info("async event invoked -> thread: " + Thread.currentThread().getName());
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	public void syncEvent() {
		log.info("sync event invoked -> thread: " + Thread.currentThread().getName());
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}
@Slf4j
@RestController
@RequestMapping("/async")
public class AsyncController {

	@Resource
	private AsyncService asyncService;

	@GetMapping("/doTask")
	public String doTask(){
		long start = System.currentTimeMillis();
		log.info("start the task:{}", start);
		// 调用同步方法
		asyncService.syncEvent();
		long syncTime = System.currentTimeMillis();
		log.info("sync event time cost:{}", syncTime - start);
		// 调用异步方法
		asyncService.asyncEvent();
		long asyncTime = System.currentTimeMillis();
		log.info("async event time cost:{}", asyncTime - syncTime);
		log.info("finish the task:{}",asyncTime);
		return "done";
	}
}

启动应用并访问http://localhost:8080/async/doTask,可看到控制台输出:

2019-01-09 11:34:04.002  INFO 8136 --- [nio-8080-exec-1] c.b.s.controller.AsyncController         : start the task:1547004844002
2019-01-09 11:34:04.005  INFO 8136 --- [nio-8080-exec-1] com.bo.springboot.service.AsyncService   : sync event invoked -> thread: http-nio-8080-exec-1
2019-01-09 11:34:07.005  INFO 8136 --- [nio-8080-exec-1] c.b.s.controller.AsyncController         : sync event time cost:3003
2019-01-09 11:34:07.013  INFO 8136 --- [nio-8080-exec-1] c.b.s.controller.AsyncController         : async event time cost:8
2019-01-09 11:34:07.014  INFO 8136 --- [nio-8080-exec-1] c.b.s.controller.AsyncController         : finish the task:1547004847013
2019-01-09 11:34:07.014  INFO 8136 --- [         task-1] com.bo.springboot.service.AsyncService   : async event invoked -> thread: task-1

可以看出,调用异步方法时,是立即返回的,基本没有耗时。这里有几点需要注意下:

  1. @Async既可以注解在类上也可以注解在方法上,当声明在类级别上时,调用类中的所有方法都将异步执行。就目标方法签名而言,支持任何参数类型。但是,返回类型被约束为void或java.util.concurrent.Future。
  2. 在默认情况下,未设置TaskExecutor时,默认是使用SimpleAsyncTaskExecutor这个线程池,但此线程不是真正意义上的线程池,因为线程不重用,每次调用都会创建一个新的线程。可通过控制台日志输出可以看出,每次输出线程名都是递增的。
  3. 调用的异步方法,不能为同一个类的方法,简单来说,因为Spring在启动扫描时会为其创建一个代理类,而同类调用时,还是调用本身的代理类的,所以和平常调用是一样的。其他的注解如@Cache等也是一样的道理,说白了,就是Spring的代理机制造成的。

自定义线程池

前面有提到,在默认情况下,系统使用的是默认的SimpleAsyncTaskExecutor进行线程创建。所以一般上我们会自定义线程池来进行线程的复用。在配置类中创建一个自定义的ThreadPoolTaskExecutor线程池:

@Configuration
@EnableAsync
public class AsyncConfig {

	@Bean
	public ThreadPoolTaskExecutor AsyncTaskExecutor() {
		ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
		taskExecutor.setCorePoolSize(10);
		taskExecutor.setMaxPoolSize(20);
		taskExecutor.setQueueCapacity(200);
		taskExecutor.setKeepAliveSeconds(60);
		taskExecutor.setThreadNamePrefix("async-task-");
		taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
		taskExecutor.initialize();
		return taskExecutor;
	}
}

再次启动应用访问http://localhost:8080/async/doTask,就可以看到已经是使用自定义的线程了:

2019-01-09 16:55:20.091  INFO 2904 --- [nio-8080-exec-1] c.b.s.controller.AsyncController         : start the task:1547024120091
2019-01-09 16:55:20.094  INFO 2904 --- [nio-8080-exec-1] com.bo.springboot.service.AsyncService   : sync event invoked -> thread: http-nio-8080-exec-1
2019-01-09 16:55:23.095  INFO 2904 --- [nio-8080-exec-1] c.b.s.controller.AsyncController         : sync event time cost:3004
2019-01-09 16:55:23.106  INFO 2904 --- [nio-8080-exec-1] c.b.s.controller.AsyncController         : async event time cost:11
2019-01-09 16:55:23.107  INFO 2904 --- [nio-8080-exec-1] c.b.s.controller.AsyncController         : finish the task:1547024123106
2019-01-09 16:55:23.107  INFO 2904 --- [   async-task-1] com.bo.springboot.service.AsyncService   : async event invoked -> thread: async-task-1

如果在配置类中定义多个线程池,异步调用的时候使用的是哪个线程池?下面在配置类中定义2个线程池进行测试:

@Configuration
@EnableAsync
public class AsyncConfig {

	@Bean
	public ThreadPoolTaskExecutor asyncTaskExecutor1() {
		ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
		taskExecutor.setCorePoolSize(10);
		taskExecutor.setMaxPoolSize(20);
		taskExecutor.setQueueCapacity(200);
		taskExecutor.setKeepAliveSeconds(60);
		taskExecutor.setThreadNamePrefix("async-task-1-");
		taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
		return taskExecutor;
	}

	@Bean
	public ThreadPoolTaskExecutor asyncTaskExecutor2() {
		ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
		taskExecutor.setCorePoolSize(10);
		taskExecutor.setMaxPoolSize(20);
		taskExecutor.setQueueCapacity(200);
		taskExecutor.setKeepAliveSeconds(60);
		taskExecutor.setThreadNamePrefix("async-task-2-");
		taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
		return taskExecutor;
	}
}

启动应用访问http://localhost:8080/async/doTask,控制台输出如下:

2019-01-09 17:04:18.116  INFO 10744 --- [nio-8080-exec-1] c.b.s.controller.AsyncController         : start the task:1547024658116
2019-01-09 17:04:18.118  INFO 10744 --- [nio-8080-exec-1] com.bo.springboot.service.AsyncService   : sync event invoked -> thread: http-nio-8080-exec-1
2019-01-09 17:04:21.118  INFO 10744 --- [nio-8080-exec-1] c.b.s.controller.AsyncController         : sync event time cost:3002
2019-01-09 17:04:21.127  INFO 10744 --- [nio-8080-exec-1] .s.a.AnnotationAsyncExecutionInterceptor : More than one TaskExecutor bean found within the context, and none is named 'taskExecutor'. Mark one of them as primary or name it 'taskExecutor' (possibly as an alias) in order to use it for async processing: [asyncTaskExecutor1, asyncTaskExecutor2]
2019-01-09 17:04:21.130  INFO 10744 --- [nio-8080-exec-1] c.b.s.controller.AsyncController         : async event time cost:12
2019-01-09 17:04:21.130  INFO 10744 --- [nio-8080-exec-1] c.b.s.controller.AsyncController         : finish the task:1547024661130
2019-01-09 17:04:21.130  INFO 10744 --- [cTaskExecutor-1] com.bo.springboot.service.AsyncService   : async event invoked -> thread: SimpleAsyncTaskExecutor-1

发现异步调用使用的不是配置类中自定义的线程池线程,而是SimpleAsyncTaskExecutor的线程,其中有一行日志说明了原因:

More than one TaskExecutor bean found within the context, and none is named 'taskExecutor'. Mark one of them as primary or name it 'taskExecutor' (possibly as an alias) in order to use it for async processing: [asyncTaskExecutor1, asyncTaskExecutor2]

 由于没有指定异步调用使用的哪一个线程池,所以最后使用了spring的默认线程池:SimpleAsyncTaskExecutor,怎么指定?

1、在其中一个自定义线程池bean添加注解@Primary或设置bean的name属性为"taskExecutor"

	@Bean
	@Primary
	public ThreadPoolTaskExecutor AsyncTaskExecutor1() ...
	@Bean(name = "taskExecutor")
	public ThreadPoolTaskExecutor AsyncTaskExecutor1() ...

2、在@Async注解的value属性指定异步调度的线程池(bean name)

	@Async("asyncTaskExecutor2")
	public void asyncEvent() ...

注意:当在类级别的@Async注解指定时,表示指定的线程池用于类中的所有方法,而方法级别的@Async注解指定的线程池始终覆盖在类级别设置的任何值。

关于ThreadPoolTaskExecutor参数说明

  • corePoolSize:线程池维护线程的最少数量

  • maxPoolSize:线程池维护线程的最大数量,只有在缓冲队列满了之后才会申请超过核心线程数的线程

  • queueCapacity:任务缓存队列

  • keepAliveSeconds:允许线程的空闲时间,当超过了核心线程数之外的线程在空闲时间到达之后会被销毁

  • threadNamePrefix:线程池中线程的名称前缀

  • rejectedExecutionHandler:线程池对拒绝任务(无线程可用)的处理策略。这里采用了CallerRunsPolicy策略,当线程池没有处理能力的时候,该策略会直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务。还有一个是AbortPolicy策略:处理程序遭到拒绝将抛出运行时RejectedExecutionException。

另外在一些场景下,若需要在关闭线程池时等待当前调度任务完成后才开始关闭,可以通过简单的配置,进行优雅的停机策略配置。关键就是通过setWaitForTasksToCompleteOnShutdown(true)setAwaitTerminationSeconds方法。

  • setWaitForTasksToCompleteOnShutdown:表明是否等待所有线程执行完任务,默认为false

  • setAwaitTerminationSeconds:等待的时间,超过这个时间就强制销毁,因为不能无限的等待下去以确保应用最后能够被关闭

所以,完整线程池配置为:

	@Bean
	public ThreadPoolTaskExecutor asyncTaskExecutor1() {
		ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
		taskExecutor.setCorePoolSize(10);
		taskExecutor.setMaxPoolSize(20);
		taskExecutor.setQueueCapacity(200);
		taskExecutor.setKeepAliveSeconds(60);
		taskExecutor.setThreadNamePrefix("async-task-1-");
		taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
		taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
		taskExecutor.setAwaitTerminationSeconds(60);
		taskExecutor.initialize();
		return taskExecutor;
	}

异步回调及超时处理

对于一些业务场景下,需要异步回调的返回值时,就需要使用异步回调来完成了。主要就是通过Future进行异步回调。

Tips:什么是Future类型?

Future是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果的接口。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。

它的接口定义如下:

public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

它声明这样的五个方法:

  • cancel方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。
  • isCancelled方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回true 
  • isDone方法表示任务是否已经完成,若任务完成,则返回true
  • get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回
  • get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,则抛出TimeoutException,结果返回null

也就是说Future提供了三种功能:

  1. 判断任务是否完成;
  2. 能够中断任务;
  3. 能够获取任务执行结果。

异步回调

新增具有Future类型返回值的异步方法和接口:

	@Async
	public Future<String> asyncRet(){
		log.info("async task invoked -> thread: {}", Thread.currentThread().getName());
		try {
			Thread.sleep(3000);
		}catch (InterruptedException e){
			e.printStackTrace();
		}
		return new AsyncResult<>("Async Result");
	}
@GetMapping("/getAsyncRet")
	public String doAsync() throws ExecutionException, InterruptedException {
		long begin = Clock.systemUTC().millis();
		log.info("start the async task: {}", begin);
		Future<String> future = asyncService.asyncRet();
		while (true){
			if(future.isDone()){
				break;
			}
		}
		long end = Clock.systemUTC().millis();
		log.info("async task time cost: {}", end - begin);
		log.info("finish the async task: {}", end);
		return future.get();
	}

其中AsyncResultSpring提供的一个Future接口的子类,然后通过isDone方法,判断是否已经执行完毕。此时,控制台输出:

2019-01-10 11:37:59.468  INFO 6672 --- [nio-8080-exec-1] c.b.s.controller.AsyncController         : start the async task: 1547091479468
2019-01-10 11:37:59.473  INFO 6672 --- [ async-task-1-1] com.bo.springboot.service.AsyncService   : async task invoked -> thread: async-task-1-1
2019-01-10 11:38:02.474  INFO 6672 --- [nio-8080-exec-1] c.b.s.controller.AsyncController         : async task time cost: 3006
2019-01-10 11:38:02.474  INFO 6672 --- [nio-8080-exec-1] c.b.s.controller.AsyncController         : finish the async task: 1547091482474

等待3秒后页面显示:

.

所以,当某个业务功能可以同时拆开一起执行时,可利用异步回调机制有效的减少程序执行时间,提高效率。

超时处理

对于一些需要异步回调的函数,不能无期限的等待下去,所以一般上需要设置超时时间,超时后可将线程释放,而不至于一直堵塞而占用资源。对于Future配置超时,很简单,通过get方法指定超时即可。

	@GetMapping("/getAsyncRetWithTimeout")
	public String getAsyncRetWithTimeout() throws InterruptedException, ExecutionException, TimeoutException {
		Future<String> future = asyncService.asyncRet();
		// 2秒后超时
		return future.get(2000, TimeUnit.MILLISECONDS);
	}

超时后,会抛出异常TimeoutException类,此时可进行统一异常捕获即可。

2019-01-10 11:51:50.598  INFO 9036 --- [ async-task-1-1] com.bo.springboot.service.AsyncService   : async task invoked -> thread: async-task-1-1
2019-01-10 11:51:52.642 ERROR 9036 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.util.concurrent.TimeoutException] with root cause

java.util.concurrent.TimeoutException: null
	at java.util.concurrent.FutureTask.get(FutureTask.java:205) ~[na:1.8.0_181]
	at com.bo.springboot.controller.AsyncController.getAsyncRetWithTimeout(AsyncController.java:65) ~[classes/:na]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_181]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_181]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_181]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_181]
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:215) ~[spring-web-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:142) ~[spring-web-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102) ~[spring-webmvc-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) ~[spring-webmvc-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:800) ~[spring-webmvc-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1038) ~[spring-webmvc-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942) ~[spring-webmvc-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:998) ~[spring-webmvc-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:890) ~[spring-webmvc-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:634) ~[tomcat-embed-core-9.0.12.jar:9.0.12]
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:875) ~[spring-webmvc-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:741) ~[tomcat-embed-core-9.0.12.jar:9.0.12]

小结

本节主要是讲解了异步调用的使用及相关配置,如超时,异常等处理。在剥离一些和业务无关的操作时,就可以考虑使用异步调用进行其他无关业务操作,以此提供业务的处理效率。或者一些业务场景下可拆分出多个方法进行同步执行又互不影响时,也可以考虑使用异步调用方式提高执行效率。

参考资料

docs.spring.io#scheduling-annotation-support-async

慕课手记:异步开发之异步调用

彻底理解Java的Future模式

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值