一、前言
通常我们开发的程序都是同步调用的,即程序按照代码的顺序一行一行的逐步往下执行,每一行代码都必须等待上一行代码执行完毕才能开始执行。而异步编程则没有这个限制,代码的调用不再是阻塞的。所以在一些情景下,通过异步编程可以提高效率,提升接口的吞吐量。这节将介绍如何在Spring Boot中进行异步编程。
二、开启异步
新建一个Spring Boot项目
2.1 引入依赖
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
|
2.2 开启异步支持
要开启异步支持,首先得在Spring Boot入口类上加上@EnableAsync注解
1 2 3 4 5 6 7 8 9
| @EnableAsync @SpringBootApplication public class AsyncApplication {
public static void main(String[] args) { SpringApplication.run(AsyncApplication.class, args); }
}
|
三、测试
3.1 创建service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Service @Slf4j public class TestService {
@Async public void asyncMethod() { sleep(); log.info("异步方法内部线程名称:{}", Thread.currentThread().getName()); }
public void syncMethod() { sleep(); }
private void sleep() { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } }
|
上面的Service中包含一个异步方法asyncMethod(开启异步支持后,只需要在方法上加上@Async注解便是异步方法了)和同步方法syncMethod。sleep方法用于让当前线程阻塞2秒钟。
3.2 创建controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| @RestController @Slf4j public class TestController { @Autowired private TestService testService;
@GetMapping("async") public void testAsync() { long start = System.currentTimeMillis(); log.info("异步方法开始");
testService.asyncMethod();
log.info("异步方法结束"); long end = System.currentTimeMillis(); log.info("总耗时:{} ms", end - start); }
@GetMapping("sync") public void testSync() { long start = System.currentTimeMillis(); log.info("同步方法开始");
testService.syncMethod();
log.info("同步方法结束"); long end = System.currentTimeMillis(); log.info("总耗时:{} ms", end - start); } }
|
3.3 测试
在http工具中访问 http://localhost:8080/sync
可看到默认程序是同步的,由于sleep方法阻塞的原因,testSync方法执行了2秒钟以上。
访问 http://localhost:8080/async
可看到testAsync方法耗时极少,因为异步的原因,程序并没有被sleep方法阻塞,这就是异步调用的好处。同时异步方法内部会新启一个线程来执行,这里线程名称为task - 1。
默认情况下的异步线程池配置使得线程不能被重用,每次调用异步方法都会新建一个线程,我们可以自己定义异步线程池来优化。
四、自定义异步线程池
4.1 创建AsyncPoolConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Configuration public class AsyncPoolConfig { @Bean public ThreadPoolTaskExecutor asyncThreadPoolTaskExecutor(){ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(20); executor.setMaxPoolSize(200); executor.setQueueCapacity(25); executor.setKeepAliveSeconds(200); executor.setThreadNamePrefix("asyncThread"); executor.setWaitForTasksToCompleteOnShutdown(true); executor.setAwaitTerminationSeconds(60);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize(); return executor; } }
|
上面我们通过ThreadPoolTaskExecutor的一些方法自定义了一个线程池,这些方法的含义如下所示:
corePoolSize:线程池核心线程的数量,默认值为1(这就是默认情况下的异步线程池配置使得线程不能被重用的原因)。
maxPoolSize:线程池维护的线程的最大数量,只有当核心线程都被用完并且缓冲队列满后,才会开始申超过请核心线程数的线程,默认值为Integer.MAX_VALUE。
queueCapacity:缓冲队列。
keepAliveSeconds:超出核心线程数外的线程在空闲时候的最大存活时间,默认为60秒。
threadNamePrefix:线程名前缀。
waitForTasksToCompleteOnShutdown:是否等待所有线程执行完毕才关闭线程池,默认值为false。
awaitTerminationSeconds:waitForTasksToCompleteOnShutdown的等待的时长,默认值为0,即不等待。
rejectedExecutionHandler:当没有线程可以被使用时的处理策略(拒绝任务),默认策略为abortPolicy,包含下面四种策略:
1>callerRunsPolicy:用于被拒绝任务的处理程序,它直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务。
2>abortPolicy:直接抛出java.util.concurrent.RejectedExecutionException异常。
3>discardOldestPolicy:当线程池中的数量等于最大线程数时、抛弃线程池中最后一个要执行的任务,并执行新传入的任务。
4>discardPolicy:当线程池中的数量等于最大线程数时,不做任何动作。
4.2 使用自定义名称
要使用该线程池,只需要在@Async注解上指定线程池Bean名称即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @Service @Slf4j public class TestService {
//@Async @Async("asyncThreadPoolTaskExecutor") public void asyncMethod() { sleep(); log.info("异步方法内部线程名称:{}", Thread.currentThread().getName()); }
public void syncMethod() { sleep(); }
private void sleep() { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } }
|
4.3 测试
访问 http://localhost:8080/async
五、处理异步回调
5.1 改造service
如果异步方法具有返回值的话,需要使用Future来接收回调值。我们在TestService新增asyncMethod2方法,给其添加返回值:
1 2 3 4 5 6
| @Async("asyncThreadPoolTaskExecutor") public Future<String> asyncMethod2() { sleep(); log.info("异步方法内部线程名称:{}", Thread.currentThread().getName()); return new AsyncResult<>("hello async"); }
|
泛型指定返回值的类型,AsyncResult为Spring实现的Future实现类:
5.2 改造controller
在TestController新增testAsync2()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @GetMapping("async2") public String testAsync2() throws Exception { long start = System.currentTimeMillis(); log.info("异步方法开始");
Future<String> stringFuture = testService.asyncMethod2(); String result = stringFuture.get(); log.info("异步方法返回值:{}", result);
log.info("异步方法结束");
long end = System.currentTimeMillis(); log.info("总耗时:{} ms", end - start); return stringFuture.get(); }
|
Future接口的get方法用于获取异步调用的返回值。
5.3 测试
重启项目,访问 http://localhost:8080/async2 控制台输出如下所示:
通过返回结果我们可以看出Future的get方法为阻塞方法,只有当异步方法返回内容了,程序才会继续往下执行。get还有一个get(long timeout, TimeUnit unit)重载方法,我们可以通过这个重载方法设置超时时间,即异步方法在设定时间内没有返回值的话,直接抛出java.util.concurrent.TimeoutException异常。
比如设置超时时间为60秒:
String result = stringFuture.get(60, TimeUnit.SECONDS);