SpringBoot 异步调用

Spring Boot 中的异步调用

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

以下分为三部分讲解:

  1. 开始异步
  2. 自定义异步线程池
  3. 处理异步回调

开启异步

  1. 添加依赖:
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

spring-boot-starter-web 中包含异步编程需要的依赖。

  1. main 方法添加注解:@EnableAsync
@SpringBootApplication
@EnableAsync
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}
  1. 编写异步方法
@Service
public class TestService {

    private static final Logger logger = LoggerFactory.getLogger(TestService.class);

   @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秒钟。

创建接口类:

@RestController
public class TestController {

    private static final Logger logger = LoggerFactory.getLogger(TestController.class);

    @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://127.0.0.1:9090/sync
输出:

2019-08-12 18:17:23.173  INFO 17996 --- [nio-9090-exec-1] S.controller.TestController              : 同步方法开始
2019-08-12 18:17:25.176  INFO 17996 --- [nio-9090-exec-1] S.controller.TestController              : 同步方法结束
2019-08-12 18:17:25.176  INFO 17996 --- [nio-9090-exec-1] S.controller.TestController              : 总耗时:2003ms

访问:http://127.0.0.1:9090/async
输出:

2019-08-12 18:17:50.942  INFO 17996 --- [nio-9090-exec-4] S.controller.TestController              : 异步方法开始
2019-08-12 18:17:50.946  INFO 17996 --- [nio-9090-exec-4] S.controller.TestController              : 异步方法结束
2019-08-12 18:17:50.946  INFO 17996 --- [nio-9090-exec-4] S.controller.TestController              : 总耗时:4ms
2019-08-12 18:17:52.946  INFO 17996 --- [   asyncThread1] SpringBootAsync.service.TestService      : 异步方法内部线程名:asyncThread1

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

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

自定义异步线程池

@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;
    }
}
  • 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`:当线程池中的数量等于最大线程数时,不做任何动作。

线程池具体细节可以查看我的另一篇博文:[深入学习]JAVA线程池

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

@Service
public class TestService {

    private static final Logger logger = LoggerFactory.getLogger(TestService.class);

    @Async("asyncThreadPoolTaskExecutor")
    public void asyncMethod() {
        sleep();
        logger.info("异步方法内部线程名:" + Thread.currentThread().getName());
    }
   ......
}

重启项目,再次访问:http://127.0.0.1:9090/async
输出:

2019-08-12 18:26:23.287  INFO 12096 --- [nio-9090-exec-1] S.controller.TestController              : 异步方法开始
2019-08-12 18:26:23.290  INFO 12096 --- [nio-9090-exec-1] S.controller.TestController              : 异步方法结束
2019-08-12 18:26:23.291  INFO 12096 --- [nio-9090-exec-1] S.controller.TestController              : 总耗时:4ms
2019-08-12 18:26:25.294  INFO 12096 --- [   asyncThread1] SpringBootAsync.service.TestService      : 异步方法内部线程名:asyncThread1

处理异步回调

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

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

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

修改接口类:

@RestController
public class TestController {

    private static final Logger logger = LoggerFactory.getLogger(TestController.class);

    @Autowired
    private TestService testService;

    @GetMapping("/async2")
    public String testAsync2() throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();
        logger.info("异步方法开始");

        Future<String> stringFuture = testService.asyncMethod2();
        logger.info("异步方法返回值:" + stringFuture.get());

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

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

        return stringFuture.get();
    }
}

Future接口的get方法用于获取异步调用的返回值。
重启项目访问:http://127.0.0.1:9090/async2

2019-08-12 18:31:15.872  INFO 17800 --- [nio-9090-exec-2] S.controller.TestController              : 异步方法开始
2019-08-12 18:31:17.879  INFO 17800 --- [   asyncThread1] SpringBootAsync.service.TestService      : 异步方法内部线程名:asyncThread1
2019-08-12 18:31:17.879  INFO 17800 --- [nio-9090-exec-2] S.controller.TestController              : 异步方法返回值:hello async
2019-08-12 18:31:17.879  INFO 17800 --- [nio-9090-exec-2] S.controller.TestController              : 异步方法结束
2019-08-12 18:31:17.879  INFO 17800 --- [nio-9090-exec-2] S.controller.TestController              : 总耗时:2007ms

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

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

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

源码链接:https://github.com/lbshold/springboot/tree/master/Spring-Boot-Async
参考文章:https://mrbird.cc/Spring-Boot-Async.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容