溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

java異步編程有哪些方式

發(fā)布時間:2021-06-28 17:21:07 來源:億速云 閱讀:214 作者:chen 欄目:編程語言

本篇內容介紹了“java異步編程有哪些方式”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!

為什么需要異步?

操作系統(tǒng)可以看作是個虛擬機(VM),進程生活在操作系統(tǒng)創(chuàng)造的虛擬世界里。進程不用知道到底有多少 core 多少內存,只要進程不要索取的太過分,操作系統(tǒng)就假裝有無限多的資源可用。

基于這個思想,線程(Thread)的個數(shù)并不受硬件限制:你的程序可以只有一個線程、也可以有成百上千個。操作系統(tǒng)會默默做好調度,讓諸多線程共享有限的 CPU 時間片。這個調度的過程對線程是完全透明的。

那么,操作系統(tǒng)是怎樣做到在線程無感知的情況下調度呢?答案是上下文切換(Context Switch),簡單來說,操作系統(tǒng)利用軟中斷機制,把程序從任意位置打斷,然后保存當前所有寄存器——包括最重要的指令寄存器 PC 和棧頂指針 SP,還有一些線程控制信息(TCB),整個過程會產生數(shù)個微秒的 overhead。

java異步編程有哪些方式

然而作為一位合格的程序員,你一定也聽說過,線程是昂貴的:

  •  線程的上下文切換有不少的代價,占用寶貴的 CPU 時間;

  •  每個線程都會占用一些(至少 1 頁)內存。

這兩個原因驅使我們盡可能避免創(chuàng)建太多的線程,而異步編程的目的就是消除 IO wait 阻塞——絕大多數(shù)時候,這是我們創(chuàng)建一堆線程、甚至引入線程池的罪魁禍首。

Continuation

回調函數(shù)知道的人很多,但了解 Continuation 的人不多。Continuation 有時被晦澀地翻譯成“計算續(xù)體”,咱們還是直接用單詞好了。

把一個計算過程在中間打斷,剩下的部分用一個對象表示,這就是 Continuation。操作系統(tǒng)暫停一個線程時保存的那些現(xiàn)場數(shù)據(jù),也可以看作一個 Continuation。有了它,我們就能在這個點接著剛剛的斷點繼續(xù)執(zhí)行。

打斷一個計算過程聽起來很厲害吧!實際上它每時每刻都在發(fā)生——假設函數(shù) f() 中間調用了 g(),那 g() 運行完成時,要返回到 f() 剛剛調用 g() 的地方接著執(zhí)行。這個過程再自然不過了,以至于所有編程語言(匯編除外)都把它掩藏起來,讓你在編程中感覺不到調用棧的存在。

java異步編程有哪些方式

操作系統(tǒng)用昂貴的軟中斷機制實現(xiàn)了棧的保存和恢復。那有沒有別的方式實現(xiàn) Continuation 呢?最樸素的想法就是,把所有用得到的信息包成一個函數(shù)對象,在調用 g() 的時候一起傳進去,并約定:一旦 g() 完成,就拿著結果去調用這個 Continuation。

這種編程模式被稱為 Continuation-passing style(CPS):

  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術社區(qū)

  2.  把調用者 f() 還未執(zhí)行的部分包成一個函數(shù)對象 cont,一同傳給被調用者 g();

  3.  正常運行 g() 函數(shù)體;

  4.  g() 完成后,連同它的結果一起回調 cont,從而繼續(xù)執(zhí)行 f() 里剩余的代碼。

再拿 Wikipedia 上的定義鞏固一下:

A function written in continuation-passing style takes an extra argument: an explicit "continuation", i.e. a function of one argument. When the CPS function has computed its result value, it "returns" it by calling the continuation function with this value as the argument.

CPS 風格的函數(shù)帶一個額外的參數(shù):一個顯式的 Continuation,具體來說就是個僅有一個參數(shù)的函數(shù)。當 CPS 函數(shù)計算完返回值時,它“返回”的方式就是拿著返回值調用那個 Continuation。

你應該已經發(fā)現(xiàn)了,這也就是回調函數(shù),我只是換了個名字而已。

異步的樸素實現(xiàn):Callback

光有回調函數(shù)其實并沒有卵用。對于純粹的計算工作,Call Stack 就很好,為何要費時費力用回調來做 Continuation 呢?你說的對,但僅限于沒有 IO 的情況。我們知道 IO 通常要比 CPU 慢上好幾個數(shù)量級,在 BIO 中,線程發(fā)起 IO 之后只能暫停,然后等待 IO 完成再由操作系統(tǒng)喚醒。

var input = recv_from_socket()  // Block at syscall recv()  var result = calculator.calculate(input)  send_to_socket(result) // Block at syscall send()

而異步 IO 中,進程發(fā)起 IO 操作時也會一并輸入回調(也就是 Continuation),這大大解放了生產力——現(xiàn)場無需等待,可以立即返回去做其他事情。一旦 IO 成功后,AIO 的 Event Loop 會調用剛剛設置的回調函數(shù),把剩下的工作完成。這種模式有時也被稱為 Fire and Forget。

recv_from_socket((input) -> {      var result = calculator.calculate(input)      send_to_socket(result) // ignore result  })

就這么簡單,通過我們自己實現(xiàn)的 Continuation,線程不再受 IO 阻塞,可以自由自在地跑滿 CPU。

一顆語法糖:Promise

回調函數(shù)哪里都好,就是不大好用,以及太丑了。

第一個問題是可讀性大大下降,由于我們繞開操作系統(tǒng)自制 Continuation,所有函數(shù)調用都要傳入一個 lambda 表達式,你的代碼看起來就像要起飛一樣,縮進止不住地往右挪(the "Callback Hell")。

第二個問題是各種細節(jié)處理起來很麻煩,比如,考慮下異常處理,看來傳一個 Continuation 還不夠,最好再傳個異常處理的 callback。

Promise 是對異步調用結果的一個封裝,在 Java 中它叫作 CompletableFuture (JDK8) 或者 ListenableFuture (Guava)。Promise 有兩層含義:

第一層含義是:我現(xiàn)在還不是真正的結果,但是承諾以后會拿到這個結果。這很容易理解,異步的任務遲早會完成,調用者如果比較蠢萌,他也可以用 Promise.get() 強行要拿到結果,順便阻塞了當前線程,異步變成了同步。

第二層含義是:如果你(調用者)有什么吩咐,就告訴我好了。這就有趣了,換句話說,回調函數(shù)不再是傳給 g(),而是 g() 返回的 Promise,比如之前那段代碼,我們用 Promise 來書寫,看起來順眼了不少。

var promise_input = recv_from_socket()  promise_input.then((input) -> {      var result = calculator.calculate(input)      send_to_socket(result) // ignore result  })

Promise 改善了 Callback 的可讀性,也讓異常處理稍稍優(yōu)雅了些,但終究是顆語法糖。

反應式編程

反應式(Reactive)最早源于函數(shù)式編程中的一種模式,隨著微軟發(fā)起 ReactiveX 項目并一步步壯大,被移植到各種語言和平臺上。Reactive 最初在 GUI 編程中有廣泛的應用,由于異步調用的高性能,很快也在服務器后端領域遍地開花。

Reactive 可以看作是對 Promise 的極大增強,相比 Promise,反應式引入了流(Flow)的概念。ReactiveX 中的事件流從一個 Observable 對象流出,這個對象可以是一個按鈕,也可以是 Restful API,總之,它能被外界觸發(fā)。與 Promise 不同的是,事件可能被觸發(fā)多次,所以處理代碼也會被多次調用。

一旦允許調用多次,從數(shù)據(jù)流動的角度看,事實上模型已經是 Push 而非 Pull。那么問題來了,如果調用頻率非常高,以至于我們處理速度跟不上了怎么辦?所以 RX 框架又引入了 Backpressure 機制來進行流控,最簡單的流控方式就是:一旦 buffer 滿,就丟棄掉之后的事件。

ReactiveX 框架的另一個優(yōu)點是內置了很多好用的算子,比如:merge(Flow 合并),debounce(開關除顫)等等,方便了業(yè)務開發(fā)。下面是一個 RxJava 的例子:

java異步編程有哪些方式

CPS 變換:Coroutine 與 async/await

無論是反應式還是 Promise,說到底仍然沒有擺脫手工構造 Continuation:開發(fā)者要把業(yè)務邏輯寫成回調函數(shù)。對于線性的邏輯基本可以應付自如,但是如果邏輯復雜一點呢?(比如,考慮下包含循環(huán)的情況)

java異步編程有哪些方式

有些語言例如 C#,JavaScript 和 Python 提供了 async/await 關鍵字。與 Reactive 一樣,這同樣出自微軟 C# 語言。在這些語言中,你會感到前所未有的爽感:異步編程終于擺脫了回調函數(shù)!唯一要做的只是在異步函數(shù)調用時加上 await,編譯器就會自動把它轉化為協(xié)程(Coroutine),而非昂貴的線程。

魔法的背后是 CPS 變換,CPS 變換把普通函數(shù)轉換成一個 CPS 的函數(shù),即 Continuation 也能作為一個調用參數(shù)。函數(shù)不僅能從頭運行,還能根據(jù) Continuation 的指示繼續(xù)某個點(比如調用 IO 的地方)運行。

可以看到,函數(shù)已經不再是一個函數(shù)了,而是變成一個狀態(tài)機。每次 call 它、或者它 call 其他異步函數(shù)時,狀態(tài)機都會做一些計算和狀態(tài)輪轉。說好的 Continuation 在哪呢?就是對象自己(this)啊。

CPS 變換實現(xiàn)非常復雜,尤其是考慮到 try-catch 之后。但是沒關系,復雜性都在編譯器里,用戶只要學兩個關鍵詞即可。這個特性非常優(yōu)雅,比 Java 那個廢柴的 CompletableFuture 不知道高到哪去了

JVM 上也有一個實現(xiàn):electronicarts/ea-async,原理和 C# 的 async/await 類似,在編譯期修改 Bytecode 實現(xiàn) CPS 變換。

終極方案:用戶態(tài)線程

有了 async/await,代碼已經簡潔很多了,基本上和同步代碼無異。是否有可能讓異步代碼和同步代碼完全一樣呢?聽起來就像免費午餐,但是的確可以做到!

用戶態(tài)線程的代表是 Golang。JVM 上也有些實現(xiàn),比如 Quasar,不過因為 JDBC、Spring 這些周邊生態(tài)(它們占據(jù)了大部分 IO 操作)的缺失基本沒有什么用。

關注公眾號Java技術棧,在后臺回復:面試,可以獲取我整理的 Java 多線程系列面試題和答案,非常齊全。

用戶態(tài)線程是把操作系統(tǒng)提供的線程機制完全拋棄,換句話說,不去用這個 VM 的虛擬化機制。比如硬件有 8 個核心,那就創(chuàng)建 8 個系統(tǒng)線程,然后把 N 個用戶線程調度到這 8 個系統(tǒng)線程上跑。N 個用戶線程的調度在用戶進程里實現(xiàn),由于一切都在進程內部,切換代價要遠遠小于操作系統(tǒng) Context Switch。

java異步編程有哪些方式

另一方面,所有可能阻塞系統(tǒng)級線程的事情,例如 sleep()、recv() 等,用戶態(tài)線程一定不能碰,否則它一旦阻塞住也就帶著那 8 個系統(tǒng)線程中的一個阻塞了。Go Runtime 接管了所有這樣的系統(tǒng)調用,并用一個統(tǒng)一的 Event loop 來輪詢和分發(fā)。

另外,由于用戶態(tài)線程很輕量,我們完全沒必要再用線程池,如果需要開線程就直接創(chuàng)建。比如 Java 中的 WebServer 幾乎一定有個線程池,而 Go 可以給每個請求開辟一個 goroutine 去處理。并發(fā)編程從未如此美好!

總結

以上方案中,Promise、Reactive 本質上還是回調函數(shù),只是框架的存在一定程度上降低了開發(fā)者的心智負擔。而 async/await 和用戶態(tài)線程的解決方案要優(yōu)雅和徹底的多,前者通過編譯期的 CPS 變換幫用戶創(chuàng)造出 CPS 式的函數(shù)調用;后者則繞開操作系統(tǒng)、重新實現(xiàn)一套線程機制,一切調度工作由 Runtime 接管。

不知道是不是因為歷史包袱太重,Java 語言本身提供的異步編程支持弱得可憐,即便是 CompletableFuture 還是在 Java 8 才引入,其后果就是很多庫都沒有異步的支持。雖然 Quasar 在沒有語言級支持的情況下引入了 CPS 變換,但是由于缺少周邊生態(tài)的支持,實際很難用在項目中。

“java異步編程有哪些方式”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關的知識可以關注億速云網(wǎng)站,小編將為大家輸出更多高質量的實用文章!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經查實,將立刻刪除涉嫌侵權內容。

AI