Spring / Spring boot 非同步任務程式設計 WebAsyncTask

擁抱心中的夢想發表於2018-06-15

今天一起學習下如何在Spring中進行非同步程式設計。我們都知道,web伺服器處理請求request的執行緒是從執行緒池中獲取的,這也不難解釋,因為當web請求併發數非常大時,如何一個請求進來就建立一條處理執行緒,由於建立執行緒和執行緒上下文切換的開銷是比較大的,web伺服器最終將面臨崩潰。另外,web伺服器建立的處理執行緒從頭到尾預設是同步執行的,也就是說,假如處理執行緒A負責處理請求B,那麼當B沒有return之前,處理執行緒A是不可以脫身去處理別的請求的,這將極大限制了web伺服器的併發處理能力。

因此執行緒池解決了執行緒可迴圈利用的問題,那同步處理請求怎麼去解決呢?答案是非同步處理。什麼是非同步處理呢?非同步處理主要是讓上面的B請求處理完成之前,能夠將A執行緒空閒出來繼續去處理別的請求。那麼我們可以這樣做,在A執行緒內部重新開啟一個執行緒C去執行任務,讓A直接返回給web伺服器,繼續接受新進來的請求。

在開始下面的講解之前,我在這裡先區別下兩個概念:

  • 1、處理執行緒

    處理執行緒屬於web伺服器,負責處理使用者請求,採用執行緒池管理

  • 2、非同步執行緒

    非同步執行緒屬於使用者自定義的執行緒,可採用執行緒池管理

spring中提供了對非同步任務的支援,採用WebAsyncTask類即可實現非同步任務,同時我們也可以對非同步任務設定相應的回撥處理,如當任務超時、丟擲異常怎麼處理等。非同步任務通常非常實用,比如我們想讓一個可能會處理很長時間的操作交給非同步執行緒去處理,又或者當一筆訂單支付完成之後,開啟非同步任務查詢訂單的支付結果。

一、正常非同步任務

為了演示方便,非同步任務的執行採用Thread.sleep(long)模擬,現在假設使用者請求以下介面 :

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執行完成啦!
複製程式碼

瀏覽器結果如下:

Spring / Spring boot 非同步任務程式設計 WebAsyncTask

二、拋異常非同步任務

介面呼叫 : 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 統一錯誤處理方案的使用

瀏覽器輸出結果:

Spring / Spring boot 非同步任務程式設計 WebAsyncTask

三、超時非同步任務

介面呼叫 : 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執行完成啦!
複製程式碼

瀏覽器執行結果:

Spring / Spring boot 非同步任務程式設計 WebAsyncTask

四、執行緒池非同步任務

上面的三種情況中的非同步任務預設不是採用執行緒池機制進行管理的,也就是說,一個請求進來,雖然釋放了處理執行緒,但是系統依舊會為每個請求建立一個非同步任務執行緒,也就是上面我們看到的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;
    }
}
複製程式碼

對上面進行併發測試,可以得出下面結果 :

Spring / Spring boot 非同步任務程式設計 WebAsyncTask

本文示例程式碼地址:github.com/SmallerCode…

採用執行緒池可以節約伺服器資源,優化伺服器處理能力,要記得常用喲!謝謝閱讀!覺得對你有幫助,請給個start哦!

相關文章