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);
            executor.setMaxPoolSize(20);
            executor.setQueueCapacity(200);
            executor.setKeepAliveSeconds(60);
            executor.setThreadNamePrefix("taskExecutor-");
            executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
            return executor;
        }
    }
  • 核心線程數10:線程池創建時候初始化的線程數
  • 最大線程數20:線程池最大的線程數,只有在緩衝隊列滿了之後纔會申請超過核心線程數的線程
  • 緩衝隊列200:用來緩衝執行任務的隊列
  • 允許線程的空閒時間60秒:當超過了核心線程出之外的線程在空閒時間到達之後會被銷燬
  • 線程池名的前綴:設置好了之後可以方便我們定位處理任務所在的線程池
  • 線程池對拒絕任務的處理策略:這裏採用了CallerRunsPolicy

還有一種寫法是去實現一個空接口AsyncConfigurer 其內部提供了初始化線程池和獲異步異常處理器

public interface AsyncConfigurer {
	/**
	 * The {@link Executor} instance to be used when processing async
	 * method invocations.
	 */
	@Nullable
	default Executor getAsyncExecutor() {
		return null;
	}
	/**
	 * The {@link AsyncUncaughtExceptionHandler} instance to be used
	 * when an exception is thrown during an asynchronous method execution
	 * with {@code void} return type.
	 */
	@Nullable
	default AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
		return null;
	}
}

這兩種常用的寫法是有一些區別的,限於篇幅我們下篇文章看@Async實現異步調用的源碼的時候再去細說。

  • 使用該線程池下的線程只需要,@Async註解中指定線程池名即可,比如:
    @Async("taskExecutor")
    public void doTaskOne() throws Exception {
        log.info("開始做任務一");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(5000));
        long end = System.currentTimeMillis();
        log.info("完成任務一,耗時:" + (end - start) + "毫秒");
    }

通過debug發現的確是使用我們自定義的線程池在執行。

關閉線程池

引入線程池也會存在不少問題,我就針對一種場景簡單說一下如何優雅的關閉線程池。

比如線程池任務還在執行,其他異步池已經停止瞭如Redis或者Mysql的連接池,此時線程池訪問就會報錯。

如何解決

在初始化線程的時候加上下面這兩句

executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
  • setWaitForTasksToCompleteOnShutdown(true):用來設置線程池關閉的時候等待所有任務都完成再繼續銷燬其他的Bean,這樣這些異步任務的銷燬就會先於Redis線程池的銷燬。
  • setAwaitTerminationSeconds(60):該方法用來設置線程池中任務的等待時間,如果超過這個時候還沒有銷燬就強制銷燬,以確保應用最後能夠被關閉,而不被阻塞住。

好了今天就說這麼多了,其實還是很簡單的運用,希望大家持續關注,後續幾天我會@Async實現異步調用的原理,以及我在開發中如何運用線程池技術縮短響應時間。🤞

更多精彩好文盡在:Java編程之道 🎁

歡迎各位好友前去關注!🌹

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章