首頁>技術>

一 、認識非同步程式設計

通常Java開發人員喜歡使用同步程式碼編寫程式,因為這種請求(request)/響應(response)的方式比較簡單,並且比較符合程式設計人員的思維習慣;這種做法很好,直到系統出現效能瓶頸;在同步程式設計方式時由於每個執行緒同時只能發起一個請求並同步等待返回,所以為了提高系統性能,此時我們就需要引入更多的執行緒來實現並行化處理;但是多執行緒下對共享資源進行訪問時,不可避免會引入資源爭用和併發問題;另外作業系統層面對執行緒的個數是有限制的,不可能通過無限的增加執行緒數來提供系統性能;最後使用同步阻塞的程式設計方式還會導致浪費資源,比如發起網路IO請求時候,呼叫執行緒就會處於同步阻塞等待響應結果的狀態,而這時候呼叫執行緒明明可以去做其他事情,等網路IO響應結果返回後在對結果進行處理。

可見通過增加單機系統執行緒個數的並行程式設計方式並不是靈丹妙藥;通過編寫非同步、非阻塞的程式碼,則可以使用相同的底層資源將執行切換到另一個活動任務,然後在非同步處理完成後在返回到當前執行緒進行繼續處理,從而提高系統性能。

非同步程式設計是可以讓程式並行執行的一種手段,其可以讓程式中的一個工作單元與主應用程式執行緒分開獨立執行,並且等工作單元執行結束後通知主應用程式執行緒它的執行結果或者失敗原因。使用它有許多好處,例如可以提高應用程式的效能和響應能力。

比如當呼叫執行緒使用非同步方式發起網路IO請求後,呼叫執行緒就不會同步阻塞等待響應結果,而是在記憶體儲存請求上下文後,會馬上返回後做其他事情,等網路IO響應結果返回後在使用IO執行緒通知業務執行緒響應結果已經返回,然後業務執行緒在對結果進行處理。可知非同步呼叫方式提高了執行緒的利用率,讓系統有更多的執行緒資源來處理更多的請求。

比如在移動應用程式中,在使用者操作移動裝置螢幕發起請求後,如果是同步等待後臺伺服器返回結果,則當後臺服務操作非常耗時時,就會造成使用者看到移動裝置螢幕凍結(一直處理請求處理中),在結果返回前,使用者不能操作移動裝置的其他功能,這對使用者體驗非常不好。而使用非同步程式設計則當發起請求後,呼叫執行緒會馬上返回,具體返回結果則會通過UI執行緒非同步進行渲染,而在這期間使用者可以使用移動裝置的其他功能。

二、 非同步程式設計場景概述

在日常開發中我們經常會遇到這樣的情況,就是需要非同步的處理一些事情,而不需要知道非同步任務的結果;比如在呼叫執行緒裡面非同步打日誌,為了不讓日誌列印阻塞呼叫執行緒,會把日誌設定為非同步方式。如下圖1-2-1日誌非同步化列印,使用一個記憶體佇列把日誌列印非同步化,使用單一執行緒來消費佇列裡面日誌事件執行具體的日誌落盤操作(本質是一個多生產單消費模型),這種情況下呼叫執行緒把日誌任務放入佇列後就繼續去幹自己的事情了,而不再關心日誌任務具體是什麼時候入盤的;

圖 1-2-1 日誌非同步列印

在Java中每當我們需要執行非同步任務的時候我們可以直接開啟一個執行緒來實現,也可以把非同步任務封裝為任務物件投遞到執行緒池裡面來執行,在Spring框架中則提供了@Async註解把一個任務非同步化來進行處理,這些內容會在後面章節具體講解。

另外有時候我們還需要在主執行緒等待非同步任務的執行結果,這時候Future就排上用場了;比如呼叫執行緒要等執行任務A執行完畢後在順序執行任務B,並且把兩者結果拼接起來作為前端展示使用,如果呼叫執行緒是同步呼叫兩次查詢(如下圖1-2-2同步呼叫),則整個過程耗時時間為執行任務A的耗時加上執行任務B的耗時。

圖1-2-2 同步呼叫

如果使用非同步程式設計(如下圖1-2-3)則可以在呼叫執行緒內開啟一個非同步執行單元來執行任務A,開啟非同步執行單元后呼叫執行緒會馬上返回一個Future物件(futureB),然後呼叫執行緒本身來執行任務B,等任務B執行完畢後,呼叫執行緒可以呼叫futureB的get()方法獲取任務A的執行結果,最後在拼接兩者結果。這時由於任務A和任務B是並行執行的,所以整個過程耗時為max(呼叫執行緒執行任務B耗時,非同步執行單元執行任務A耗時)。

圖1-2-3 非同步呼叫

可見整個過程耗時有顯著縮短,對於使用者來說頁面響應時間會更短,對使用者體驗會更好,其中非同步單元的執行一般是執行緒池中的執行緒。

使用Future確實可以獲取非同步任務的執行結果,但是獲取其結果還是會阻塞呼叫執行緒的,並沒有實現完全非同步化處理,在JDK8中提供了CompletableFuture來彌補了其缺點。CompletableFuture類允許以非阻塞方式和基於通知的方式處理結果,其通過設定回撥函式方式,讓主執行緒徹底解放出來,做自己的事情,實現了實際意義上的非同步處理;

如下圖1-2-4使用CompletableFuture時候當非同步單元返回futureB後,呼叫執行緒可以在其上呼叫whenComplete方法設定一個回撥函式action,然後呼叫執行緒就會馬上返回了,等非同步任務執行完畢後會使用非同步執行緒來執行回撥函式action,而無需呼叫執行緒干預,如果你對CompletableFuture不了解,沒關係,後面章節我們會詳細講解,這裡你只需要知道其解決了傳統Future的缺陷就可以了。

圖1-2-4 CompletableFuture非同步執行

JDK8還引入了Stream,它旨在有效地處理資料流(包括原始型別),其使用宣告式程式設計讓我們可以寫出可讀性、可維護性很強的程式碼,並且結合CompletableFuture可以完美的實現非同步程式設計。但是它產生的流只能使用一次,並且缺少與時間相關的操作(例如RxJava中的基於時間視窗的快取元素),雖然可以執行平行計算,但無法指定要使用的執行緒池。並且它還沒有設計用於處理延遲的操作(例如RxJava中的defer操作);而Reactor或RxJava等Reactive API就是為了解決這些問題而生的。

Reactor或RxJava等反應式API也提供Java 8 Stream的運算子,但它們更適用於任何流序列(不僅僅是集合),並允許定義一個轉換操作的管道,該管道將應用於通過它的資料,這要歸功於方便的流暢API和Lambda表示式的使用。Reactive旨在處理同步或非同步操作,並允許您緩衝(buffer)、合併(merge)、連線(join) 元素等對元素做各種轉換。

上面我們講解了單JVM內的非同步程式設計,那麼對於跨網路的互動是否也存在非同步程式設計範疇那?對於網路請求來說,同步呼叫時比較直截了當的,比如我們在一個執行緒A中通過RPC請求獲取服務B和服務C的資料,然後基於兩者結果做一些事情。在同步呼叫情況下,執行緒A需要呼叫服務B,然後需要同步等待服務B結果返回後,才可以對服務C發起呼叫,然後等服務C結果返回後才可以結合服務B和C的結果做一件事,如下圖1-2-5:

圖1-2-5 同步RPC呼叫

如上圖1-2-5執行緒A同步獲取服務B結果後,在同步呼叫服務C獲取結果,可見在同步呼叫情況下業務執行語義比較清晰,執行緒A順序的對多個服務請求進行呼叫;但是同步呼叫意味著當前發起請求的呼叫執行緒在遠端機器返回結果前必須阻塞等待,這明顯很浪費資源。好的做法應該是發起請求的呼叫執行緒發起請求後,註冊一個回撥函式,然後馬上返回去做其他事情,當遠端把結果返回後在使用IO執行緒執行回撥函式。

那麼如何實現非同步呼叫?在Java中NIO的出現讓實現上面的功能變得簡單,而高效能非同步、基於事件驅動的網路程式設計框架Netty的出現讓我們從編寫繁雜的Java NIO程式出解放出來了,現在的RPC框架比如Dubbo底層網路通訊就是基於Netty實現的;Netty框架將網路程式設計邏輯與業務邏輯處理分離開來,其內部幫我們自動處理好網路與非同步處理邏輯,讓我們專心寫自己的業務處理邏輯,Netty的非同步非阻塞能力與CompletableFuture結合就可以輕鬆的實現網路請求的非同步呼叫。

在執行RPC(遠端過程呼叫)呼叫時候,使用非同步程式設計可以提高系統的效能;如下圖1-2-6,在非同步呼叫情況下,當執行緒A呼叫服務B後,馬上會返回一個非同步的futureB物件,然後執行緒A可以在futureB上設定一個回撥函式;然後執行緒A可以繼續訪問服務C,也會馬上返回一個futureC物件,然後執行緒A可以在futureC上設定一個回撥函式:

圖1-2-6 RPC非同步呼叫

如上圖1-2-6可知非同步呼叫情況下執行緒A可以併發的呼叫服務B和服務C,而不再是順序的,由於服務B和服務C是併發執行,所以相比執行緒A同步呼叫,執行緒A獲取到服務B和服務C結果的時間會縮短很多(同步呼叫情況下耗時時間為服務B和服務C返回結果耗時的和,非同步呼叫時候耗時為max(服務B耗時,服務C耗時));另外這裡可以藉助CompletableFuture的能力等兩次RPC呼叫都非同步返回結果後做一件事情,這時候呼叫流程如下圖圖1-2-7:

圖1-2-7 合併Rpc呼叫結果

如上圖圖1-2-7呼叫執行緒A首先發起服務B的遠端呼叫,然後馬上返回一個futureB物件,然後發起服務C的遠端呼叫,然後馬上返回一個futureC物件,最後呼叫執行緒A使用程式碼futureB.thenCombine(futureC,action)等futureB和futureC結果可用時候執行回撥函式action;這裡我們只是簡單的概述下基於Netty的非同步非阻塞能力以及CompletableFuture的可編排能力,我們可以實現功能很強大的非同步程式設計能力,後面章節我們會以Dubbo框架為例講解其藉助Netty的非阻塞非同步API實現了服務消費端的非同步呼叫。

其實有了CompletableFuture實現非同步程式設計,我們可以很自然的使用介面卡來實現Reactive風格的程式設計,當我們使用RxJava API時候我們只需要使用Flowable的一些函式轉換CompletableFuture為Flowable物件即可,這個我們在後面章節也會講述。

上節講解了網路請求中的RPC框架的非同步請求,其實還有一類,也就是Web請求,在Web應用中Servlet佔有一席之地。在Servlet3.0規範前,Servlet容器對Servlet的處理都是每個請求對應一個執行緒這種1:1的模式進行處理的(如下圖1-2-8),每當來一個請求時候都會開啟一個Servlet容器內的執行緒來進行處理,如果Servlet內處理比較耗時,則會把Servlet容器內執行緒使用耗盡,然後容器就不能再處理新的請求。

圖1-2-8 Servlet的阻塞處理模型

Servlet3.0規範中則提供了非同步處理的能力,讓Servlet容器中的執行緒可以及時釋放,具體Servlet業務處理邏輯是在業務自己執行緒池內來處理;雖然Servlet3.0規範讓Servlet的執行變為了非同步,但是其IO還是阻塞式的,IO阻塞是說在Servlet處理請求時候從ServletInputStream中讀取請求體時候是阻塞的,而我們想要的是當資料已經就緒時候通知我們去讀取就可以了,因為這可以避免佔用我們自己的執行緒來進行阻塞讀取,Servlet3.1規範則提供了非阻塞IO來解決這個問題。

雖然Servlet技術棧的不斷髮展實現了非同步處理與非阻塞IO,但是其非同步是不徹底的,因為受制於Servlet規範本身,比如其規範是同步的(Filter,Servlet)或阻塞(getParameter,getPart)。所以新的使用少量執行緒和較少的硬體資源來處理併發的非阻塞Web技術棧應運而生-WebFlux,其是與Servlet技術棧並行存在的一種新的技術,其基於JDK8函數語言程式設計與Netty實現天然的非同步、非阻塞處理,這些我們在後面章節會具體介紹。

另外為了更好的處理非同步程式設計,降低我們非同步程式設計的成本,一些框架也應運而生,比如高效能執行緒間訊息傳遞庫Disruptor,其通過為事件(events)預先分配記憶體、無鎖CAS演算法、緩衝行填充、兩階段協議提交來實現多執行緒併發的處理不同的元素,從而實現高效能的非同步處理;比如Akka其基於Actor模式實現了天然支援分散式的使用訊息進行非同步處理的服務;比如高效能分散式訊息中介軟體Apache RocketMetaQ用來實現應用間的非同步解耦、流量削峰。

一些新興的語言對非同步處理的支援能力讓我們忍不住稱讚,GoLang就是其中之一,其通過語言層面內建的goroutine與channel可以輕鬆的實現複雜的非同步處理能力。

三、 為何寫作本書

非同步程式設計是可以讓程式並行執行的一種手段,其可以讓程式中的一個工作單元與主應用程式執行緒分開獨立執行,使用它有許多好處,例如可以提高應用程式的效能和響應能力。

雖然Java中不同技術域提供了相應的非同步程式設計技術,但是對非同步程式設計技術的描述散落到了不同技術域的技術文件中,並沒有一個統一的地方對這些技術進行梳理歸納。另外這些技術之間是什麼關係,各自的出現都是為了解決什麼問題,我們也很難找到資料來解釋。

本書的出現則是為了打破這種局面,本書旨在把Java中相關的非同步程式設計技術進行歸納分類總結,然後呈現給大家,讓大家可以有一個統一的地方來檢視與探究。

四、本書特色

本書涵蓋了Java中常見的非同步程式設計場景,這包含單JVM內的非同步程式設計、以及跨主機通過網路通訊的遠端過程呼叫的非同步呼叫與非同步處理、以及Web請求的非同步處理等等。

本書在講解Java中每種非同步程式設計技術時都附有案例,以便理論與實踐進行結合。

本書在講解每種非同步程式設計技術時大多都會對其實現原理進行講解,以便讓讀者知其然也知其所以然。

本書對最近比較火的反應式程式設計以及WebFlux的使用與原理解析有一定深入的探索。

最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • Vue基礎教程