先贊後看,養成習慣 🌹 歡迎微信關注[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編程之道 🎁
歡迎各位好友前去關注!🌹