認識Java異步編程

一 、認識異步編程

一般Java開發人員喜歡使用同步代碼編寫程序,由於這種請求(request)/響應(response)的方式比較簡單,而且比較符合編程人員的思惟習慣;這種作法很好,直到系統出現性能瓶頸;在同步編程方式時因爲每一個線程同時只能發起一個請求並同步等待返回,因此爲了提升系統性能,此時咱們就須要引入更多的線程來實現並行化處理;可是多線程下對共享資源進行訪問時,不可避免會引入資源爭用和併發問題;另外操做系統層面對線程的個數是有限制的,不可能經過無限的增長線程數來提供系統性能;最後使用同步阻塞的編程方式還會致使浪費資源,好比發起網絡IO請求時候,調用線程就會處於同步阻塞等待響應結果的狀態,而這時候調用線程明明能夠去作其餘事情,等網絡IO響應結果返回後在對結果進行處理。html

可見經過增長單機系統線程個數的並行編程方式並非靈丹妙藥;經過編寫異步、非阻塞的代碼,則可使用相同的底層資源將執行切換到另外一個活動任務,而後在異步處理完成後在返回到當前線程進行繼續處理,從而提升系統性能。前端

異步編程是可讓程序並行運行的一種手段,其可讓程序中的一個工做單元與主應用程序線程分開獨立運行,而且等工做單元運行結束後通知主應用程序線程它的運行結果或者失敗緣由。使用它有許多好處,例如能夠提升應用程序的性能和響應能力。算法

好比當調用線程使用異步方式發起網絡IO請求後,調用線程就不會同步阻塞等待響應結果,而是在內存保存請求上下文後,會立刻返回後作其餘事情,等網絡IO響應結果返回後在使用IO線程通知業務線程響應結果已經返回,而後業務線程在對結果進行處理。可知異步調用方式提升了線程的利用率,讓系統有更多的線程資源來處理更多的請求。編程

好比在移動應用程序中,在用戶操做移動設備屏幕發起請求後,若是是同步等待後臺服務器返回結果,則當後臺服務操做很是耗時時,就會形成用戶看到移動設備屏幕凍結(一直處理請求處理中),在結果返回前,用戶不能操做移動設備的其餘功能,這對用戶體驗很是很差。而使用異步編程則當發起請求後,調用線程會立刻返回,具體返回結果則會經過UI線程異步進行渲染,而在這期間用戶可使用移動設備的其餘功能。緩存

2、 異步編程場景概述

在平常開發中咱們常常會遇到這樣的狀況,就是須要異步的處理一些事情,而不須要知道異步任務的結果;好比在調用線程裏面異步打日誌,爲了避免讓日誌打印阻塞調用線程,會把日誌設置爲異步方式。以下圖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異步編程實戰》),一書則是根據上述介紹的次序,把內容劃分了若干章節,每章則具體展開討論相應的異步編程技術。

3、 爲什麼寫做本書

異步編程是可讓程序並行運行的一種手段,其可讓程序中的一個工做單元與主應用程序線程分開獨立運行,使用它有許多好處,例如能夠提升應用程序的性能和響應能力。

雖然Java中不一樣技術域提供了相應的異步編程技術,可是對異步編程技術的描述散落到了不一樣技術域的技術文檔中,並無一個統一的地方對這些技術進行梳理概括。另外這些技術之間是什麼關係,各自的出現都是爲了解決什麼問題,咱們也很難找到資料來解釋。

本書的出現則是爲了打破這種局面,本書旨在把Java中相關的異步編程技術進行概括分類總結,而後呈現給你們,讓你們能夠有一個統一的地方來查看與探究。

4、本書特點

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

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

本書在講解每種異步編程技術時大多都會對其實現原理進行講解,以便讓讀者知其然也知其因此然。

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

5、 業界推薦

8.png


本文做者:加多

閱讀原文

本文爲阿里雲內容,未經容許不得轉載。

相關文章
相關標籤/搜索