解構反應式程式設計——Java8,RxJava,Reactor之比較
如果你熟悉Java 8,同時又瞭解反應式程式設計(Reactive Programming)框架,例如RxJava和Reactor等,你可能會問:
“如果我可以用Java 8 的Stream, CompletableFuture, 以及Optional完成同樣的事情,為什麼還要用RxJava 或者 Reactor呢?”
問題在於,大多數時候你在處理的是簡單的任務,這個時候你確實不需要那些反應式程式設計的庫。但是,當系統越來越複雜,或者你處理的本身就是個複雜的任務,你恐怕就得寫一些讓自己頭皮發麻的程式碼。隨著時間的推移,這些程式碼會變得越來越複雜和難以維護。
RxJava和Reactor提供了很多非常趁手的功能,能夠支援你在未來更輕鬆地維護你的程式碼,實現新需求。但是這個優勢到底有多大,具體體現在哪些方面?沒有標準無法比較,讓我們定義8個比較的維度,來幫助我們理解Java 8的API以及反應式程式設計的庫之間的差別。
- Composable(可組裝)
- Lazy(延遲執行)
- Reusable(可重用)
- Asynchronous(非同步)
- Cacheable(可快取)
- Push or Pull(推還是拉)
- Backpressure(反壓)
- Operator fusion(操作融合)
針對上面這些維度,我們比較以下的這些類:
- CompletableFuture
- Stream
- Optional
- Observable (RxJava 1)
- Observable (RxJava 2)
- Flowable (RxJava 2)
- Flux (Reactor Core)
準備好了嗎?我們開始!
Composable(可組裝)
上面所有的這7個類都是可組裝的,支援函式式的程式設計方式,這是我們喜歡它們的原因。
- CompletableFuture – 提供很多的.then*()方法,這些方法允許我們構建一個流水線,在不同的執行階段之間傳遞一個單一的值(或者沒有值),以及傳遞異常物件。
- Stream – 提供很多的可以鏈式程式設計方式連線起來的操作,不同的操作階段之間可以傳遞N個值。
- Optional – 提供一些中間操作,如: .map(), .flatMap(), .filter().
- Observable, Flowable, Flux – 跟Stream相同
Lazy(延遲執行)
- CompletableFuture – 非延遲執行,它本質上只是一個非同步結果的持有者。這些物件建立出來是為了代表對應的工作,CompletableFuture建立的時候,對應的工作已經開始執行了。它不知道任何的關於工作的具體內容,只是關心結果。所以,沒有辦法能走到上游去從上到下執行整個流水線。當結果被塞到CompletableFuture物件的時候,下一個階段開始執行。
- Stream – 所有的中間操作都是延遲執行的。所有的終端操作,會觸發整個計算。
- Optional – 非延遲執行,所有的操作會馬上發生。
- Observable, Flowable, Flux – 延遲執行,沒有訂閱者的話,什麼都不會做,只有當有訂閱者的時候才會執行。
Reusable(可重用)
- CompletableFuture – 可以重用,它只是在一個值外面做了一層包裝。但需要注意一點,這個包裝是可更改的。.obtrude*()方法會更改它的內容,如果你確定沒有人會呼叫到這類方法,那麼重用它還是安全的。
- Stream – 不能重用。Java Doc已經說了:
A stream should be operated on (invoking an intermediate or terminal stream operation) only once. A stream implementation may throw IllegalStateException if it detects that the stream is being reused. However, since some stream operations may return their receiver rather than a new stream object, it may not be possible to detect reuse in all cases.
翻譯過來就是:Stream只能被操作(呼叫中間操作或者終端操作)一次。如果一個stream的實現檢測到流被重複使用了,它可以丟擲一個IllegalStateException。但是因為某些流操作會返回他們的receiver,而不是一個新的stream物件,並不是在所有的情況下都能夠檢測出重用。
- Optional – 完全可重用,因為它是不可變物件,而且所有工作都是立即執行的。
- Observable, Flowable, Flux – 就是設計來可重用的。所有的執行會從初始點開始,走過所有階段,前提是有訂閱者。
Asynchronous(非同步)
-
CompletableFuture – 嗯…這個類存在的目的就是非同步的把多個操作連結起來。CompletableFuture代表一個工作,後面跟一個Executor關聯起來。如果你不明確指定一個executor,那麼系統會使用公共的ForkJoinPool執行緒池來執行。這個執行緒池可以用ForkJoinPool.commonPool()獲取到。預設的設定下它會建立系統硬體支援的執行緒數一樣多的執行緒(通常就是跟CPU的核心數,如果你的CPU支援超執行緒,那麼可能再翻一倍)。不過你也可以設定ForkJoinPool執行緒池的執行緒數,用以下JVM option:
-Djava.util.concurrent.ForkJoinPool.common.parallelism=?
或者每次呼叫的時候提供一個定製的Executor。
- Stream – 不支援建立非同步過程,但是可以支援並行的計算——通過stream.parallel()等方式建立並行流。
- Optional – 不支援,它只是一個容器。
- Observable, Flowable, Flux – 目標就是為了構建非同步的系統,但是預設情況下還是同步的。subscribeOn和observeOn允許你來控制訊息的訂閱以及訊息的接收(指定當你的observer的 onNext / onError / onCompleted 被呼叫的時候做什麼事情)。
subscribeOn讓你決定用哪個Scheduler來執行Observable.create。即便你自己沒有呼叫create,系統內部也會做類似的事情。示例:
Observable
.fromCallable(() -> {
log.info("Reading on thread: " + currentThread().getName());
return readFile("input.txt");
})
.map(text -> {
log.info("Map on thread: " + currentThread().getName());
return text.length();
})
.subscribeOn(Schedulers.io()) // <-- setting scheduler
.subscribe(value -> {
log.info("Result on thread: " + currentThread().getName());
});
輸出:
Reading file on thread: RxIoScheduler-2
Map on thread: RxIoScheduler-2
Result on thread: RxIoScheduler-2
相反的,observeOn()決定在observeOn()之後,用哪個Scheduler來執行下游的執行階段。示例:
Observable
.fromCallable(() -> {
log.info("Reading on thread: " + currentThread().getName());
return readFile("input.txt");
})
.observeOn(Schedulers.computation()) // <-- setting scheduler
.map(text -> {
log.info("Map on thread: " + currentThread().getName());
return text.length();
})
.subscribeOn(Schedulers.io()) // <-- setting scheduler
.subscribe(value -> {
log.info("Result on thread: " + currentThread().getName());
});
輸出:
Reading file on thread: RxIoScheduler-2
Map on thread: RxComputationScheduler-1
Result on thread: RxComputationScheduler-1
Cacheable(可快取)
可快取和可重用之間的區別是什麼?舉個例子,我們有一個流水線A,並且使用這個流水線兩次,建立兩個新的流水線 B = A + 以及 C = A + 。
– 如果B和C都能成功完成,那麼這個A是可重用的。
– 如果B和C都能成功完成,並且A的每一個階段只被呼叫了一次,那麼這個A是可快取的。
可以看出,一個類如果是可快取的,必然得是可重用的。
- CompletableFuture – 跟可重用的答案一樣。
- Stream – 不能快取中間操作的結果,除非呼叫了終端操作。
- Optional – ‘可快取’,實際上,所有工作立即執行,並且做完後就儲存了一個不變值,自然‘可快取’。
- Observable, Flowable, Flux – 預設情況下是不可快取的,但是你可以把一個這些類轉變成快取,只要呼叫.cache()就可以。示例:
Observable<Integer> work = Observable.fromCallable(() -> {
System.out.println("Doing some work");
return 10;
});
work.subscribe(System.out::println);
work.map(i -> i * 2).subscribe(System.out::println);
輸出:
Doing some work
10
Doing some work
20
如果用.cache():
Observable<Integer> work = Observable.fromCallable(() -> {
System.out.println("Doing some work");
return 10;
}).cache(); // <- apply caching
work.subscribe(System.out::println);
work.map(i -> i * 2).subscribe(System.out::println);
輸出:
Doing some work
10
20
Push or Pull(推模式還是拉模式)
- Stream 和 Optional – 是拉模式的。你呼叫不同的方法(.get(), .collect() 等)從流水線拉取結果。拉模式經常與阻塞、同步是相關聯的,而這也合理。你呼叫一個方法,然後執行緒等待資料。執行緒會阻塞直到資料到達。
- CompletableFuture, Observable, Flowable, Flux – 是推模式的。你訂閱一個流水線,然後當有東西可以處理的時候你會得到通知。推模式通常意味著非阻塞、非同步。當流水線在某個執行緒上執行的時候,你可以做任何事情。你已經定義了一段待執行的程式碼,作為下一個階段的任務,當通知到達的時候,這個程式碼就會被執行。
Backpressure(反壓)
要做到支援反壓,流水線必須是推模式的。
Backpressure(反壓) 描述的是在流水線中會發生的一種場景:某些非同步的階段處理速度跟不上,需要告訴上游生產者放慢速度。直接失敗是不可接受的,因為會丟失太多資料。
- Stream & Optional – 不支援反壓,因為他們是拉模式。
- CompletableFuture – 不需要面對這個問題,因為它只產生0個或者1個結果。
- Observable(RxJava 1), Flowable, Flux – 提供一組方案解決這個問題。常用的策略是:
– Buffering(緩衝) – 把所有的onNext的值儲存到緩衝區,直到下游消費它們。
– Drop Recent – 如果下游處理跟不上的話,丟棄最近的onNext值。
– Use Latest – 如果下游處理跟不上的話,只提供最近的onNext值,之前的值會被覆蓋。
– None – onNext事件直接被觸發,不帶任何緩衝或丟棄處理。
– Exception – 如果下游處理跟不上的話,觸發一個異常。
- Observable(RxJava 2) – 不解決這個問題。很多RxJava 1的使用者用Observable來處理不適用反壓的事件,或者是使用Observable的時候不用任何策略處理反壓,這會導致不可預知的異常。所以,RxJava 2明確地區分兩種情況,提供支援反壓的Flowable和不支援反壓的Observable。
Operator fusion(操作融合)
操作融合背後的想法是,在生命週期的不同點上,改變執行階段的鏈條,從而消除庫的架構因素所造成的額外開銷。所有這些優化都是在內部處理掉的,對外部使用者來說是透明的。
只有RxJava 2 和 Reactor 支援這個特性,但支援的方式不同。總的來說,有兩種型別的優化:
- Macro-fusion – 用一個操作替換2個或更多的相繼的操作
- Micro-fusion – 在一個輸出佇列中結束的操作,和在一個前驅佇列中開始的操作,能夠共用同一個佇列的例項。比如說,與其呼叫request(1)然後處理onOnext(),我們可以:
訂閱者可以向父observable輪詢值。
更多的詳細資訊可以參考Operator-fusion (part 1) 和 Operator-fusion (part 2)
總結
上面的內容可以總結為一個表:
總的來說,Stream, CompletableFuture, 和 Optional建立出來是為了解決特定的問題。它們解決這些問題很好用。如果它們滿足你的要求,你繼續用它們就好了。
但是,不同的問題有不同的複雜性。某些問題需要新的技術。RxJava 和 Reactor是一組通用的工具,幫助你用一種宣告式的方式解決你面對的問題,而不是用一些並非為這種問題而提供的工具,來建立一種“hack”的解決方案。
本文主要內容翻譯自:http://alexsderkach.io/comparing-java-8-rxjava-reactor/
相關文章
- 聊聊Spring Reactor反應式程式設計SpringReact程式設計
- 使用Reactor響應式程式設計React程式設計
- 響應式程式設計RxJava (一)程式設計RxJava
- 響應式程式設計簡介之:Reactor程式設計React
- 高效能網路程式設計-反應堆模型(reactor)程式設計模型React
- 響應式程式設計入門(RxJava)程式設計RxJava
- 響應式程式設計庫RxJava初探程式設計RxJava
- 設計模式 in real world - 反應器模式(Reactor Pattern)設計模式React
- 八個層面比較 Java 8, RxJava, ReactorRxJavaReact
- .Net 中的反應式程式設計程式設計
- 什麼是反應式程式設計?程式設計
- Java程式設計方法論-響應式 之 Rxjava篇 視訊解讀程式設計RxJava
- 反應式程式設計讀書筆記程式設計筆記
- Java反應式框架Reactor中的Mono和FluxJava框架ReactMonoUX
- mysql——ROUND與TRUNCATE函式之比較MySql函式
- 反應式程式設計在微服務下的重生程式設計微服務
- 快速瞭解非同步程式設計 RxJava非同步程式設計RxJava
- 反應式程式設計是正確的方法嗎? - JAXenter程式設計
- 深入淺出RxJava(四)-在Android中使用響應式程式設計RxJavaAndroid程式設計
- 深入淺出RxJava(4):在Android中使用響應式程式設計RxJavaAndroid程式設計
- 使用Project Reactor進行反應式資料流 - spring.ioProjectReactSpring
- Python 與 Javascript 之比較PythonJavaScript
- C# Java之比較 (轉)C#Java
- Java8函數語言程式設計應用Java函數程式設計
- 【筆記】RDBMS和NOSQL之比較筆記SQL
- Free BSD,Linux之比較(轉)Linux
- 12種JavaScript MVC框架之比較JavaScriptMVC框架
- 完美解釋 Javascript 響應式程式設計原理JavaScript程式設計
- Node.JS程式設計師的反應Node.js程式設計師
- 使用反應式程式設計替換Java自動資源管理 - Arvind程式設計Java
- 網路程式設計-Netty-Reactor模型程式設計NettyReact模型
- Java8 CompletableFuture 程式設計Java程式設計
- 兩種高效能I/O設計模式(Reactor/Proactor)的比較設計模式React
- 響應式程式設計與MVVM架構—理論篇程式設計MVVM架構
- 通俗解釋什麼是響應式程式設計?程式設計
- C++、Java/C#之比較 (轉)C++JavaC#
- 好程式設計師分享java8新特性之Lambda表示式程式設計師Java
- 揚帆起航:從指令式程式設計到函式響應式程式設計程式設計函式