今天一塊兒學習下如何在Spring中進行異步編程。咱們都知道,web服務器處理請求request
的線程是從線程池中獲取的,這也不難解釋,由於當web請求併發數很是大時,如何一個請求進來就建立一條處理線程,因爲建立線程和線程上下文切換的開銷是比較大的,web服務器最終將面臨崩潰。另外,web服務器建立的處理線程從頭至尾默認是同步執行的,也就是說,假如處理線程A負責處理請求B,那麼當B沒有return
以前,處理線程A是不能夠脫身去處理別的請求的,這將極大限制了web服務器的併發處理能力。java
所以線程池解決了線程可循環利用的問題,那同步處理請求怎麼去解決呢?答案是異步處理。什麼是異步處理呢?異步處理主要是讓上面的B請求處理完成以前,可以將A線程空閒出來繼續去處理別的請求。那麼咱們能夠這樣作,在A線程內部從新開啓一個線程C去執行任務,讓A直接返回給web服務器,繼續接受新進來的請求。git
在開始下面的講解以前,我在這裏先區別下兩個概念:github
一、處理線程web
處理線程屬於web服務器,負責處理用戶請求,採用線程池管理spring
二、異步線程編程
異步線程屬於用戶自定義的線程,可採用線程池管理json
spring中提供了對異步任務的支持,採用WebAsyncTask
類便可實現異步任務,同時咱們也能夠對異步任務設置相應的回調處理,如當任務超時、拋出異常怎麼處理等。異步任務一般很是實用,好比咱們想讓一個可能會處理很長時間的操做交給異步線程去處理,又或者當一筆訂單支付完成以後,開啓異步任務查詢訂單的支付結果。瀏覽器
爲了演示方便,異步任務的執行採用Thread.sleep(long)
模擬,如今假設用戶請求如下接口 :bash
http://localhost:7000/demo/getUserWithNoThing.json
服務器
異步任務接口定義以下:
/**
* 測試沒有發生任何異常的異步任務
*/
@RequestMapping(value = "getUserWithNoThing.json", method = RequestMethod.GET)
public WebAsyncTask<String> getUserWithNoThing() {
// 打印處理線程名
System.err.println("The main Thread name is " + Thread.currentThread().getName());
// 此處模擬開啓一個異步任務,超時時間爲10s
WebAsyncTask<String> task1 = new WebAsyncTask<String>(10 * 1000L, () -> {
System.err.println("The first Thread name is " + Thread.currentThread().getName());
// 任務處理時間5s,不超時
Thread.sleep(5 * 1000L);
return "任務1順利執行成功!任何異常都沒有拋出!";
});
// 任務執行完成時調用該方法
task1.onCompletion(() -> {
System.err.println("任務1執行完成啦!");
});
System.err.println("task1繼續處理其餘事情!");
return task1;
}
複製代碼
控制檯打印以下:
The main Thread name is http-nio-7000-exec-1
task1繼續處理其餘事情!
The first Thread name is MvcAsync1
任務1執行完成啦!
複製代碼
瀏覽器結果以下:
接口調用 : http://localhost:7000/demo/getUserWithError.json
/**
* 測試發生error的異步任務
* @return
*/
@RequestMapping(value = "getUserWithError.json", method = RequestMethod.GET)
public WebAsyncTask<String> getUserWithError() {
System.err.println("The main Thread name is " + Thread.currentThread().getName());
// 此處模擬開啓一個異步任務
WebAsyncTask<String> task3 = new WebAsyncTask<String>(10 * 1000L, () -> {
System.err.println("The second Thread name is " + Thread.currentThread().getName());
// 此處拋出異常
int num = 9 / 0;
System.err.println(num);
return "";
});
// 發生異常時調用該方法
task3.onError(() -> {
System.err.println("====================================" + Thread.currentThread().getName()
+ "==============================");
System.err.println("任務3發生error啦!");
return "";
});
// 任務執行完成時調用該方法
task3.onCompletion(() -> {
System.err.println("任務3執行完成啦!");
});
System.err.println("task3繼續處理其餘事情!");
return task3;
}
複製代碼
控制檯輸出以下:
The main Thread name is http-nio-7000-exec-1
task3繼續處理其餘事情!
The second Thread name is MvcAsync1
2018-06-15 09:40:13.538 ERROR 9168 --- [nio-7000-exec-2] o.a.c.c.C.[.[.[.[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] threw exception
java.lang.ArithmeticException: / by zero
at com.example.demo.controller.GetUserInfoController.lambda$5(GetUserInfoController.java:93) ~[classes/:na]
at org.springframework.web.context.request.async.WebAsyncManager.lambda$startCallableProcessing$4(WebAsyncManager.java:317) ~[spring-web-5.0.6.RELEASE.jar:5.0.6.RELEASE]
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) ~[na:1.8.0_161]
at java.util.concurrent.FutureTask.run(FutureTask.java:266) ~[na:1.8.0_161]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_161]
2018-06-15 09:40:13.539 ERROR 9168 --- [nio-7000-exec-2] o.a.c.c.C.[.[.[.[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [/demo] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause
java.lang.ArithmeticException: / by zero
at com.example.demo.controller.GetUserInfoController.lambda$5(GetUserInfoController.java:93) ~[classes/:na]
at org.springframework.web.context.request.async.WebAsyncManager.lambda$startCallableProcessing$4(WebAsyncManager.java:317) ~[spring-web-5.0.6.RELEASE.jar:5.0.6.RELEASE]
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) ~[na:1.8.0_161]
at java.util.concurrent.FutureTask.run(FutureTask.java:266) ~[na:1.8.0_161]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_161]
====================================http-nio-7000-exec-2==============================
任務3發生error啦!
任務3執行完成啦!
複製代碼
固然你也能夠對上面作一些異常處理,不至於在用戶看來顯得不友好,關於異常處理,能夠查看個人另外一篇博文Spring boot/Spring 統一錯誤處理方案的使用
瀏覽器輸出結果:
接口調用 : http://localhost:7000/demo/getUserWithTimeOut.json
/**
* 測試發生任務超時的異步任務
* @return
*/
@RequestMapping(value = "getUserWithTimeOut.json", method = RequestMethod.GET)
public WebAsyncTask<String> getUserWithTimeOut() {
System.err.println("The main Thread name is " + Thread.currentThread().getName());
// 此處模擬開啓一個異步任務,超時10s
WebAsyncTask<String> task2 = new WebAsyncTask<String>(10 * 1000L, () -> {
System.err.println("The second Thread name is " + Thread.currentThread().getName());
Thread.sleep(20 * 1000L);
return "任務2執行超時!";
});
// 任務超時調用該方法
task2.onTimeout(() -> {
System.err.println("====================================" + Thread.currentThread().getName()
+ "==============================");
return "任務2發生超時啦!";
});
// 任務執行完成時調用該方法
task2.onCompletion(() -> {
System.err.println("任務2執行完成啦!");
});
System.err.println("task2繼續處理其餘事情!");
return task2;
}
複製代碼
控制檯執行結果:
The main Thread name is http-nio-7000-exec-4
task2繼續處理其餘事情!
The second Thread name is MvcAsync2
====================================http-nio-7000-exec-5==============================
任務2執行完成啦!
複製代碼
瀏覽器執行結果:
上面的三種狀況中的異步任務默認不是採用線程池機制進行管理的,也就是說,一個請求進來,雖然釋放了處理線程,可是系統依舊會爲每一個請求建立一個異步任務線程,也就是上面咱們看到的MvcAsync
開頭的異步任務線程,那這樣不行啊,開銷特別大呀!因此咱們能夠採用線程池進行管理,直接在WebAsyncTask
類構造器傳入一個ThreadPoolTaskExecutor
對象實例便可。
下面咱們先看看,當對上面第一種狀況執行併發請求時會出現什麼狀況(此處模擬對http://localhost:7000/demo/getUserWithNoThing.json
進行併發調用):
控制檯輸出以下:
The first Thread name is MvcAsync57
The first Thread name is MvcAsync58
The first Thread name is MvcAsync59
The first Thread name is MvcAsync60
The first Thread name is MvcAsync61
The first Thread name is MvcAsync62
The first Thread name is MvcAsync63
The first Thread name is MvcAsync64
The first Thread name is MvcAsync65
The first Thread name is MvcAsync66
The first Thread name is MvcAsync67
The first Thread name is MvcAsync68
The first Thread name is MvcAsync69
The first Thread name is MvcAsync70
The first Thread name is MvcAsync71
The first Thread name is MvcAsync72
The first Thread name is MvcAsync73
The first Thread name is MvcAsync74
The first Thread name is MvcAsync76
The first Thread name is MvcAsync75
The first Thread name is MvcAsync77
The first Thread name is MvcAsync78
The first Thread name is MvcAsync79
The first Thread name is MvcAsync80
複製代碼
因爲沒有加入線程池,因此100個請求將開啓100個異步任務線程,開銷特別大,不推薦。
下面是採用線程池的實現 :
調用接口 : http://localhost:7000/demo/getUserWithExecutor.json
/**
* 測試線程池
* @return
*/
@RequestMapping(value = "getUserWithExecutor.json", method = RequestMethod.GET)
public WebAsyncTask<String> getUserWithExecutor() {
System.err.println("The main Thread name is " + Thread.currentThread().getName());
// 此處模擬開啓一個異步任務,此處傳入一個線程池
WebAsyncTask<String> task1 = new WebAsyncTask<String>(10 * 1000L, executor, () -> {
System.err.println("The first Thread name is " + Thread.currentThread().getName());
Thread.sleep(5000L);
return "任務4順利執行成功!任何異常都沒有拋出!";
});
// 任務執行完成時調用該方法
task1.onCompletion(() -> {
System.err.println("任務4執行完成啦!");
});
System.err.println("task4繼續處理其餘事情!");
return task1;
}
複製代碼
線程池定義以下:
@Configuration
public class MyExecutor {
@Bean
public static ThreadPoolTaskExecutor getExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(30);
taskExecutor.setMaxPoolSize(30);
taskExecutor.setQueueCapacity(50);
taskExecutor.setThreadNamePrefix("huang");// 異步任務線程名以 huang 爲前綴
return taskExecutor;
}
}
複製代碼
對上面進行併發測試,能夠得出下面結果 :
本文示例代碼地址:github.com/SmallerCode…
採用線程池能夠節約服務器資源,優化服務器處理能力,要記得經常使用喲!謝謝閱讀!以爲對你有幫助,請給個start哦!