天天看點

認識Java異步程式設計一 、認識異步程式設計二、 異步程式設計場景概述三、 為何寫作本書四、本書特色五、 業界推薦

一 、認識異步程式設計

通常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中相關的異步程式設計技術進行歸納分類總結,然後呈現給大家,讓大家可以有一個統一的地方來檢視與探究。

四、本書特色

本書涵蓋了Java中常見的異步程式設計場景,這包含單JVM内的異步程式設計、以及跨主機通過網絡通訊的遠端過程調用的異步調用與異步處理、以及Web請求的異步處理等等。

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

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

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

五、 業界推薦