Spring Boot 中的异步调用

通常我们开发的程序都是同步调用的,即程序按照代码的顺序一行一行的逐步往下执行,每一行代码都必须等待上一行代码执行完毕才能开始执行。而异步编程则没有这个限制,代码的调用不再是阻塞的。所以在一些情景下,通过异步编程可以提高效率,提升接口的吞吐量。这节将介绍如何在Spring Boot中进行异步编程。

开启异步

新建一个Spring Boot项目,版本为2.1.0.RELEASE,并引入​​spring-boot-starter-web​​依赖,项目结构如下所示:

Spring Boot 中的异步调用_默认值

要开启异步支持,首先得在Spring Boot入口类上加上​​@EnableAsync​​注解:


1
2
3
4
5
6
7



@SpringBootApplication
@EnableAsync
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}


 

接下来开始编写异步方法。

在​​com.example.demo​​路径下新建​​service​​包,并创建​​TestService​​:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23



@Service
public class TestService {

private Logger logger = LoggerFactory.getLogger(this.getClass());

@Async
public void asyncMethod() {
sleep();
logger.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秒钟。

接着在​​com.example.demo​​路径下新建​​controller​​包,然后创建​​TestController​​:


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
31
32



@RestController
public class TestController {

private Logger logger = LoggerFactory.getLogger(this.getClass());

@Autowired
private TestService testService;

@GetMapping("async")
public void testAsync() {
long start = System.currentTimeMillis();
logger.info("异步方法开始");

testService.asyncMethod();

logger.info("异步方法结束");
long end = System.currentTimeMillis();
logger.info("总耗时:{} ms", end - start);
}

@GetMapping("sync")
public void testSync() {
long start = System.currentTimeMillis();
logger.info("同步方法开始");

testService.syncMethod();

logger.info("同步方法结束");
long end = System.currentTimeMillis();
logger.info("总耗时:{} ms", end - start);
}
}


 

启动项目,访问 http://localhost:8080/sync 请求,控制台输出如下:

Spring Boot 中的异步调用_spring_02

可看到默认程序是同步的,由于​​sleep​​方法阻塞的原因,​​testSync​​方法执行了2秒钟以上。

访问 http://localhost:8080/async ,控制台输出如下:

Spring Boot 中的异步调用_spring_03

可看到​​testAsync​​方法耗时极少,因为异步的原因,程序并没有被​​sleep​​方法阻塞,这就是异步调用的好处。同时异步方法内部会新启一个线程来执行,这里线程名称为task - 1。

默认情况下的异步线程池配置使得线程不能被重用,每次调用异步方法都会新建一个线程,我们可以自己定义异步线程池来优化。

自定义异步线程池

在​​com.example.demo​​下新建​​config​​包,然后创建​​AsyncPoolConfig​​配置类:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20



@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。
  • awaitTerminationSecondswaitForTasksToCompleteOnShutdown的等待的时长,默认值为0,即不等待。
  • rejectedExecutionHandler:当没有线程可以被使用时的处理策略(拒绝任务),默认策略为abortPolicy,包含下面四种策略:
  1. callerRunsPolicy:用于被拒绝任务的处理程序,它直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务。
  2. abortPolicy:直接抛出java.util.concurrent.RejectedExecutionException异常。
  3. discardOldestPolicy:当线程池中的数量等于最大线程数时、抛弃线程池中最后一个要执行的任务,并执行新传入的任务。
  4. discardPolicy:当线程池中的数量等于最大线程数时,不做任何动作。

要使用该线程池,只需要在​​@Async​​注解上指定线程池Bean名称即可:


1
2
3
4
5
6
7
8
9
10



@Service
public class TestService {
......

@Async("asyncThreadPoolTaskExecutor")
public void asyncMethod() {
......
}
......
}


重启项目,再次访问 http://localhost:8080/async ,控制台输出入下:

Spring Boot 中的异步调用_线程池_04

处理异步回调

如果异步方法具有返回值的话,需要使用​​Future​​来接收回调值。我们修改​​TestService​​的​​asyncMethod​​方法,给其添加返回值:


1
2
3
4
5
6



@Async("asyncThreadPoolTaskExecutor")
public Future<String> asyncMethod() {
sleep();
logger.info("异步方法内部线程名称:{}", Thread.currentThread().getName());
return new AsyncResult<>("hello async");
}


 

泛型指定返回值的类型,​​AsyncResult​​为Spring实现的​​Future​​实现类:

Spring Boot 中的异步调用_spring_05

接着改造​​TestController​​的​​testAsync​​方法:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15



@GetMapping("async")
public String testAsync() throws Exception {
long start = System.currentTimeMillis();
logger.info("异步方法开始");

Future<String> stringFuture = testService.asyncMethod();
String result = stringFuture.get();
logger.info("异步方法返回值:{}", result);

logger.info("异步方法结束");

long end = System.currentTimeMillis();
logger.info("总耗时:{} ms", end - start);
return stringFuture.get();
}


 

​Future​​接口的​​get​​方法用于获取异步调用的返回值。

重启项目,访问 http://localhost:8080/async 控制台输出如下所示:

Spring Boot 中的异步调用_异步线程_06

通过返回结果我们可以看出​​Future​​的​​get​​方法为阻塞方法,只有当异步方法返回内容了,程序才会继续往下执行。​​get​​还有一个​​get(long timeout, TimeUnit unit)​​重载方法,我们可以通过这个重载方法设置超时时间,即异步方法在设定时间内没有返回值的话,直接抛出​​java.util.concurrent.TimeoutException​​异常。

比如设置超时时间为60秒:


1



String result = stringFuture.get(60, TimeUnit.SECONDS);