- Java異步編程實戰
- 翟陸續
- 3323字
- 2020-01-15 10:22:28
1.2 異步編程場景
在日常開發中我們經常會遇到這樣的情況,即需要異步地處理一些事情,而不需要知道異步任務的結果。比如在調用線程里面異步打日志,為了不讓日志打印阻塞調用線程,會把日志設置為異步方式。如圖1-1所示的日志異步化打印,使用一個內存隊列把日志打印異步化,然后使用單一消費線程異步處理內存隊列中的日志事件,執行具體的日志落盤操作(本質是一個多生產單消費模型),在這種情況下,調用線程把日志任務放入隊列后會繼續執行其他操作,而不再關心日志任務具體是什么時候入盤的。

圖1-1 日志異步打印
在Java中,每當我們需要執行異步任務時,可以直接開啟一個線程來實現,也可以把異步任務封裝為任務對象投遞到線程池中來執行。在Spring框架中提供了@Async注解把一個任務異步化來進行處理,具體會在后面章節詳細講解。
有時候我們還需要在主線程等待異步任務的執行結果,這時候Future就派上用場了。比如調用線程要等任務A執行完畢后再順序執行任務B,并且把兩者的執行結果拼接起來供前端展示使用,如果調用線程是同步調用兩次任務(如圖1-2所示),則整個過程耗時為執行任務A的耗時加上執行任務B的耗時。

圖1-2 同步調用
如果使用異步編程(如圖1-3所示),則可以在調用線程內開啟一個異步運行單元來執行任務A,開啟異步運行單元后調用線程會馬上返回一個Future對象(futureB),然后調用線程本身來執行任務B,等任務B執行完畢后,調用線程可以調用futureB的get()方法獲取任務A的執行結果,最后再拼接兩者的結果。這時由于任務A和任務B是并行運行的,所以整個過程耗時為max(調用線程執行任務B的耗時,異步運行單元執行任務A的耗時)。

圖1-3 異步調用
可見整個過程耗時顯著縮短,對于用戶來說,頁面響應時間縮短,用戶體驗會更好,其中異步單元的執行一般是由線程池中的線程執行。
使用Future確實可以獲取異步任務的執行結果,但是獲取其結果還是會阻塞調用線程的,并沒有實現完全異步化處理,所以在JDK8中提供了CompletableFuture來彌補其缺點。CompletableFuture類允許以非阻塞方式和基于通知的方式處理結果,其通過設置回調函數方式,讓主線程徹底解放出來,實現了實際意義上的異步處理。
如圖1-4所示,使用CompletableFuture時,當異步單元返回futureB后,調用線程可以在其上調用whenComplete方法設置一個回調函數action,然后調用線程就會馬上返回,等異步任務執行完畢后會使用異步線程來執行回調函數action,而無須調用線程干預。如果你對CompletableFuture不了解,沒關系,后面章節我們會詳細講解,這里你只需要知道其解決了傳統Future的缺陷就可以了。

圖1-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-5所示,線程A同步獲取服務B的結果后,再同步調用服務C獲取結果,可見在同步調用情況下業務執行語義比較清晰,線程A順序地對多個服務請求進行調用;但是同步調用意味著當前發起請求的調用線程在遠端機器返回結果前必須阻塞等待,這明顯很浪費資源。好的做法應該是在發起請求的調用線程發起請求后,注冊一個回調函數,然后馬上返回去執行其他操作,當遠端把結果返回后再使用IO線程或框架線程池中的線程執行回調函數。

圖1-5 同步RPC調用
那么如何實現異步調用?在Java中NIO的出現讓實現上面的功能變得簡單,而高性能異步、基于事件驅動的網絡編程框架Netty的出現讓我們從編寫繁雜的Java NIO程序中解放出來,現在的RPC框架,比如Dubbo底層網絡通信,就是基于Netty實現的。Netty框架將網絡編程邏輯與業務邏輯處理分離開來,在內部幫我們自動處理好網絡與異步處理邏輯,讓我們專心寫自己的業務處理邏輯,而Netty的異步非阻塞能力與CompletableFuture結合則可以輕松地實現網絡請求的異步調用。
在執行RPC(遠程過程調用)調用時,使用異步編程可以提高系統的性能。如圖1-6所示,在異步調用情況下,當線程A調用服務B后,會馬上返回一個異步的futureB對象,然后線程A可以在futureB上設置一個回調函數;接著線程A可以繼續訪問服務C,也會馬上返回一個futureC對象,然后線程A可以在futureC上設置一個回調函數。

圖1-6 RPC異步調用
如圖1-6可知,在異步調用情況下,線程A可以并發地調用服務B和服務C,而不再是順序的。由于服務B和服務C是并發運行,所以相比同步調用,線程A獲取到服務B和服務C結果的時間會縮短很多(同步調用情況下的耗時為服務B和服務C返回結果耗時的和,異步調用情況下耗時為max(服務B耗時,服務C耗時))。另外,這里可以借助CompletableFuture的能力等兩次RPC調用都異步返回結果后再執行其他操作,這時候調用流程如圖1-7所示。

圖1-7 合并RPC調用結果
如圖1-7所示,調用線程A首先發起服務B的遠程調用,會馬上返回一個futureB對象,然后發起服務C的遠程調用,也會馬上返回一個futureC對象,最后調用線程A使用代碼futureB.thenCombine(futureC, action)等futureB和futureC結果可用時執行回調函數action。這里我們只是簡單概述下基于Netty的異步非阻塞能力以及Completable-Future的可編排能力,基于這些能力,我們可以實現功能很強大的異步編程能力。在后面章節,我們會以Dubbo框架為例講解其借助Netty的非阻塞異步API實現服務消費端的異步調用。
其實,有了CompletableFuture實現異步編程,我們可以很自然地使用適配器來實現Reactive風格的編程。當我們使用RxJava API時,只需要使用Flowable的一些函數轉換CompletableFuture為Flowable對象即可,這個我們在后面章節也會講述。
上節講解了網絡請求中RPC框架的異步請求,其實還有一類,也就是Web請求,在Web應用中Servlet占有一席之地。在Servlet3.0規范前,Servlet容器對Servlet的處理都是每個請求對應一個線程這種1 : 1的模式進行處理的(如圖1-8所示),每當收到一個請求,都會開啟一個Servlet容器內的線程來進行處理,如果Servlet內處理比較耗時,則會把Servlet容器內線程使用耗盡,然后容器就不能再處理新的請求了。

圖1-8 Servlet的阻塞處理模型
Servlet 3.0規范中則提供了異步處理的能力,讓Servlet容器中的線程可以及時釋放,具體Servlet業務處理邏輯是在業務自己的線程池內來處理;雖然Servlet 3.0規范讓Servlet的執行變為了異步,但是其IO還是阻塞式的。IO阻塞是說在Servlet處理請求時,從ServletInputStream中讀取請求體時是阻塞的,而我們想要的是當數據就緒時直接通知我們去讀取就可以了,因為這可以避免占用我們自己的線程來進行阻塞讀取,好在Servlet 3.1規范提供了非阻塞IO來解決這個問題。
雖然Servlet技術棧的不斷發展實現了異步處理與非阻塞IO,但是其異步是不徹底的,因為受制于Servlet規范本身,比如其規范是同步的(Filter, Servlet)或阻塞的(getParameter, getPart)。所以新的使用少量線程和較少的硬件資源來處理并發的非阻塞Web技術棧應運而生—WebFlux,其是與Servlet技術棧并行存在的一種新技術,基于JDK8函數式編程與Netty實現天然的異步、非阻塞處理,這些我們在后面章節會具體介紹。
為了更好地處理異步編程,降低異步編程的成本,一些框架也應運而生,比如高性能線程間消息傳遞庫Disruptor,其通過為事件(event)預先分配內存、無鎖CAS算法、緩沖行填充、兩階段協議提交來實現多線程并發地處理不同的元素,從而實現高性能的異步處理。比如Akka基于Actor模式實現了天然支持分布式的使用消息進行異步處理的服務;比如高性能分布式消息中間件Apache RocketMetaQ實現了應用間的異步解耦、流量削峰。
一些新興的語言對異步處理的支持能力讓我們忍不住稱贊,Go語言就是其中之一,其通過語言層面內置的goroutine與channel可以輕松實現復雜的異步處理能力。
以上就是本書要討論的內容,根據上述介紹的順序,書中將內容劃分為若干章節,每章則具體展開討論相應的異步編程技術。