专栏名称: 爱唠嗑的阿磊
Java研发工程师 | 公众号-[Java编程之道]
目录
相关文章推荐
上城区市场监管局  ·  “食”分安心丨元宵前夕严查市场秩序 ... ·  昨天  
中国药闻  ·  拼出“开门红” 各地稳步有序复工复产 ·  昨天  
51好读  ›  专栏  ›  爱唠嗑的阿磊

SpringBoot如何使用@Async实现异步调用

爱唠嗑的阿磊  · 掘金  ·  · 2020-07-07 14:47

正文

阅读 29

SpringBoot如何使用@Async实现异步调用

先赞后看,养成习惯 🌹 欢迎微信关注[Java编程之道],每天进步一点点,沉淀技术分享知识。

预祝各位正在高考的小学弟学妹们考上理想的大学, 高考加油
学长忠告:报志愿千万 选计算机啊~🙄

今天我们聊一下SpringBoot中的异步技术中的 异步线程池 ,这一块的内容深入的聊内容还是很多的,所以暂时分为三个部分

  • 使用 @Async 实现异步调用以及自定义线程池的实现。
  • SpringBoot中异步调用线程池 内部实现原理
  • 我是如何通过线程池技术将 10s 的任务降低到 ms 级别。

话不多说跟紧我,老司机要发车了!

异步调用

异步调用这个概念对于学过Java基础的同学来说并不陌生,下面我们以两端代码来直观看看异步和同步的区别以及SpringBoot中实现异步调用的方式。

同步任务

/**
 * @Auther: 爱唠嗑的阿磊
 * @Company: Java编程之道
 * @Date: 2020/7/7 20:12
 * @Version 1.0
 */
@Component
public class MyTask {
    public static Random random =new Random();

    public void doTaskOne() throws Exception {
        System.out.println("开始做任务一");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(5000));
        long end = System.currentTimeMillis();
        System.out.println("完成任务一,耗时:" + (end - start) + "毫秒");
    }

    public void doTaskTwo() throws Exception {
        System.out.println("开始做任务二");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(5000));
        long end = System.currentTimeMillis();
        System.out.println("完成任务二,耗时:" + (end - start) + "毫秒");
    }

    public void doTaskThree() throws Exception {
        System.out.println("开始做任务三");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(5000));
        long end = System.currentTimeMillis();
        System.out.println("完成任务三,耗时:" + (end - start) + "毫秒");
    }
}
复制代码

注入MyTask对象,执行三个函数。

 @RestController
    class Test{
        @Autowired
        MyTask myTask;
        @GetMapping("/")
        public void contextLoads() throws Exception {
            myTask.doTaskOne();
            myTask.doTaskTwo();
            myTask.doTaskThree();
        }
    }
复制代码

访问http://127.0.0.1:8080/可以看到类似如下输出:

开始做任务一
完成任务一,耗时:3387毫秒
开始做任务二
完成任务二,耗时:621毫秒
开始做任务三
完成任务三,耗时:4395毫秒
复制代码

异步调用

接下来就通过SpringBoot中的异步调用技术,使三个不存在依赖关系的任务实现并发执行。在Spring Boot中,最简单的方式是通过@Async注解将原来的同步函数变为异步函数.

/**
 * @Auther: 爱唠嗑的阿磊
 * @Company: Java编程之道
 * @Date: 2020/7/7 20:12
 * @Version 1.0
 */
@Component
public class MyTask {
    public static Random random =new Random();
    @Async
    public void doTaskOne() throws Exception {
        System.out.println("开始做任务一");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(5000));
        long end = System.currentTimeMillis();
        System.out.println("完成任务一,耗时:" + (end - start) + "毫秒");
    }
    @Async
    public void doTaskTwo() throws Exception {
        System.out.println("开始做任务二");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(5000));
        long end = System.currentTimeMillis();
        System.out.println("完成任务二,耗时:" + (end - start) + "毫秒");
    }
    @Async
    public void doTaskThree() throws Exception {
        System.out.println("开始做任务三");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(5000));
        long end = System.currentTimeMillis();
        System.out.println("完成任务三,耗时:" + (end - start) + "毫秒");
    }
}
复制代码

同时需要在Spring Boot的主程序中配置@EnableAsync使@Async注解能够生效

@EnableAsync
@SpringBootApplication
public class ThreaddemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(ThreaddemoApplication.class, args);
    }
}
复制代码

再次测试执行你会发现响应结果明显快了不少,但是数据的顺序是乱的。原因是三个函数候已经是异步执行了。主程序在异步调用执行之后,线程的执行顺序得不到保障。

这里可以想到为什么我在 V-LoggingTool 使用可配置的开启的线程池了,因为我存储日志并不关心线程任务的返回值,我需要程序立即往下执行,耗时任务交给线程池去执行就行了。

如果一定要拿到线程执行的结果,对于这个问题怎么处理简单来说 看场景 ,可以使用Future的get来阻塞获取结果从而保证得到正确的数据。对于一些超时任务的场景可以在get中设置超时时间。

异步回调

接着上文所说的解决思路我们可以通过Future来返回异步调用的结果来感知线程是否执行结束并且获取返回值。知道Future/Callable的同学应该不会感到很陌生。

将三个方法都这样处理一下

/**
 * @Auther: 爱唠嗑的阿磊
 * @Company: Java编程之道
 * @Date: 2020/7/7 20:12
 * @Version 1.0
 */
@Component
public class MyTask {
    public static Random random =new Random();
    @Async
    public Future<String> doTaskOne() throws Exception {
        System.out.println("开始做任务一");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(5000));
        long end = System.currentTimeMillis();
        System.out.println("完成任务一,耗时:" + (end - start) + "毫秒");
        return new AsyncResult<>("任务一完成");
    }
    @Async
    public Future<String> doTaskTwo() throws Exception {
        System.out.println("开始做任务二");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(5000));
        long end = System.currentTimeMillis();
        System.out.println("完成任务二,耗时:" + (end - start) + "毫秒");
        return new AsyncResult<>("任务二完成");
    }
    @Async
    public Future<String> doTaskThree() throws Exception {
        System.out.println("开始做任务三");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(5000));
        long end = System.currentTimeMillis();
        System.out.println("完成任务三,耗时:" + (end - start) + "毫秒");
        return new AsyncResult<>("任务三完成");
    }
}

复制代码

改造一下测试类

 @RestController
    class Test{
        @Autowired
        MyTask myTask;
        @GetMapping("/")
        public void contextLoads() throws Exception {
            /*myTask.doTaskOne();
            myTask.doTaskTwo();
            myTask.doTaskThree();*/
            long start = System.currentTimeMillis();
            Future<String> task1 = myTask.doTaskOne();
            Future<String> task2 = myTask.doTaskTwo();
            Future<String> task3 = myTask.doTaskThree();
            task1.get();
            task2.get();
            task3.get();
            long end = System.currentTimeMillis();
            System.out.println("任务全部完成,总耗时:" + (end - start) + "毫秒");
        }
    }
复制代码

执行一下

开始做任务三
开始做任务一
开始做任务二
完成任务三,耗时:1125毫秒
完成任务二,耗时:1520毫秒
完成任务一,耗时:4344毫秒
任务全部完成,总耗时:4354毫秒
复制代码

当然我只是举一个获取异步回调的例子,实质上,上诉这种写法不可取,因为get是一个阻塞方法,task1如果一直不执行完的话就会一直阻塞在这里。同理还可以使用其他技术来保证一个合理的返回值如: CountDownLatch 等。

自定义线程池

在SpirngBoot中实现自定义线程池很简单,没有接触通过注解实现异步的时候,大家都是自己去写一个线程池然后注入到容器中,最后暴露一下任务提交的方法...但是SpringBoot为你省去了很多繁杂的操作。

  • 第一步,先在配置类中定义一个线程池
@EnableAsync
    @Configuration
    class TaskPoolConfig {
        @Bean("taskExecutor")
        public Executor taskExecutor() {
            ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
            executor.setCorePoolSize(10






请到「今天看啥」查看全文