八個層面比較 Java 8, RxJava, Reactor

Kirito的技術分享發表於2019-01-22

前言

這是一篇譯文,原文出處 戳這裡。其實很久以前我就看完了這篇文章,只不過個人對響應式程式設計研究的不夠深入,羞於下筆翻譯,在加上這類譯文加了原創還有爭議性,所以一直沒有動力。恰逢今天交流群裡兩個大佬對響應式程式設計的話題辯得不可開交,趁印象還算深刻,藉機把這篇文章翻譯一下。說道辯論的點,不妨也在這裡丟擲來:

響應式程式設計在單機環境下是否雞肋?

結論是:沒有結論,我覺得只能抱著懷疑的眼光審視這個問題了。另外還聊到了 RSocket 這個最近在 SpringOne 大會上比較火爆的響應式"新“網路協議,github 地址戳這裡,為什麼給”新“字打了個引號,仔細觀察下 RSocket 的 commit log,其實三年前就有了。有興趣的同學自行翻閱,說不定就是今年這最後兩三個月的熱點技術哦。

Java 圈子有一個怪事,那就是對 RxJava,Reactor,WebFlux 這些響應式程式設計的名詞、框架永遠處於渴望瞭解,感到新鮮,卻又不甚瞭解,使用貧乏的狀態。之前轉載小馬哥的那篇《Reactive Programming 一種技術,各自表述》時,就已經聊過這個關於名詞之爭的話題了,今天群裡的討論更是加深了我的映像。Java 圈子裡面很多朋友一直對響應式程式設計處於一個瞭解名詞,知道基本原理,而不是深度使用者的狀態(我也是之一)。可能真的和圈子有關,按石衝兄的說法,其實 Scala 圈子裡面的那幫人,不知道比我們們高到哪裡去了(就響應式程式設計而言)。

實在是好久沒發文章了,向大家說聲抱歉,以後的更新頻率肯定是沒有以前那麼勤了(說的好像以前很勤快似的),一部分原因是在公司內網寫的文章沒法貼到公眾號中和大家分享討論,另一部分是目前我也處於學習公司內部框架的階段,不太方便提煉成文章,最後,最大的一部分原因還是我這段時間需要學(tou)習(lan)其(da)他(you)東(xi)西啦。好了,廢話也說完了,下面是譯文的正文部分。

引言

關於響應式程式設計(Reactive Programming),你可能有過這樣的疑問:我們已經有了 Java8 的 Stream, CompletableFuture, 以及 Optional,為什麼還必要存在 RxJava 和 Reactor?

回答這個問題並不難,如果在響應式程式設計中處理的問題非常簡單,你的確不需要那些第三方類庫的支援。 但隨著複雜問題的出現,你寫出了一堆難看的程式碼。然後這些程式碼變得越來越複雜,難以維護,而 RxJava 和 Reactor 具有許多方便的功能,可以解決你當下問題,並保障了未來一些可預見的需求。本文從響應式程式設計模型中抽象出了8個標準,這將有助於我們理解標準特性與這些庫之間的區別:

  1. Composable(可組合)
  2. Lazy(惰性執行)
  3. Reusable(可複用)
  4. Asynchronous(非同步)
  5. Cacheable(可快取)
  6. Push or Pull(推拉模型)
  7. Backpressure(回壓)(譯者注:按照石衝老哥的建議,這個詞應當翻譯成"回壓"而不是"背壓")
  8. Operator fusion(操作融合)

我們將會對以下這些類進行這些特性的對比:

  1. CompletableFuture(Java 8)
  2. Stream(Java 8)
  3. Optional(Java 8)
  4. Observable (RxJava 1)
  5. Observable (RxJava 2)
  6. Flowable (RxJava 2)
  7. Flux (Reactor Core)

讓我們開始吧~

1. Composable(可組合)

這些類都是支援 Composable 特性的,使得各位使用者很便利地使用函數語言程式設計的思想去思考問題,這也正是我們擁躉它們的原因。

CompletableFuture - 眾多的 .then*() 方法使得我們可以構建一個 pipeline, 用以傳遞空值,單一的值,以及異常.

Stream - 提供了許多鏈式操作的程式設計介面,支援在各個操作之間傳遞多個值。

Optional - 提供了一些中間操作 .map(), .flatMap(), .filter().

Observable, Flowable, Flux - 和 Stream 相同

2. Lazy(惰性執行)

CompletableFuture - 不具備惰性執行的特性,它本質上只是一個非同步結果的容器。這些物件的建立是用來表示對應的工作,CompletableFuture 建立時,對應的工作已經開始執行了。但它並不知道任何工作細節,只關心結果。所以,沒有辦法從上至下執行整個 pipeline。當結果被設定給 CompletableFuture 時,下一個階段才開始執行。

Stream - 所有的中間操作都是延遲執行的。所有的終止操作(terminal operations),會觸發真正的計算(譯者注:如 collect() 就是一個終止操作)。

Optional - 不具備惰性執行的特性,所有的操作會立刻執行。

Observable, Flowable, Flux - 惰性執行,只有當訂閱者出現時才會執行,否則不執行。

3. 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 只能被呼叫一次。如果被校測到流被重複使用了,它會跑出丟擲一個 IllegalStateException 異常。但是某些流操作會返回他們的接受者,而不是一個新的流物件,所以無法在所有情況下檢測出是否可以重用)

Optional - 完全可重用,因為它是不可變物件,而且所有操作都是立刻執行的。

Observable, Flowable, Flux - 生而重用,專門設計成如此。當存在訂閱者時,每一次執行都會從初始點開始完整地執行一邊。

4. Asynchronous(非同步)

CompletableFuture - 這個類的要點在於它非同步地把多個操作連線了起來。CompletableFuture 代表一項操作,它會跟一個 Executor 關聯起來。如果不明確指定一個 Executor,那麼會預設使用公共的 ForkJoinPool 執行緒池來執行。這個執行緒池可以用 ForkJoinPool.commonPool() 獲取到。預設設定下它會建立系統硬體支援的執行緒數一樣多的執行緒(通常和 CPU 的核心數相等,如果你的 CPU 支援超執行緒(hyperthreading),那麼會設定成兩倍的執行緒數)。不過你也可以使用 JVM 引數指定 ForkJoinPool 執行緒池的執行緒數,

-Djava.util.concurrent.ForkJoinPool.common.parallelism=?
複製程式碼

或者在建立 CompletableFuture 時提供一個指定的 Executor。

Stream - 不支援建立非同步執行流程,但是可以使用 stream.parallel() 等方式建立並行流。

Optional - 不支援,它只是一個容器。

Observable, Flowable, Flux - 專門設計用以構建非同步系統,但預設情況下是同步的。subscribeOnobserveOn允許你來控制訂閱以及接收(這個執行緒會呼叫 observer 的 onNext / onError / onCompleted方法)。

subscribeOn 方法使得你可以決定由哪個 Scheduler 來執行 Observable.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
複製程式碼

5. Cacheable(可快取)

可快取和可複用之間的區別是什麼?假如我們有 pipeline A,重複使用它兩次,來建立兩個新的 pipeline B = A + X 以及 C = A + Y

  • 如果 B 和 C 都能成功執行,那麼這個 A 就是是可重用的。
  • 如果 B 和 C 都能成功執行,並且 A 在這個過程中,整個 pipeline 只執行了一次,那麼我們便稱 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
複製程式碼

6. Push or Pull(推拉模型)

Stream 和 Optional - 拉模型。呼叫不同的方法(.get(), .collect() 等)從 pipeline 拉取結果。拉模型通常和阻塞、同步關聯,那也是公平的。當呼叫方法時,執行緒會一直阻塞,直到有資料到達。

CompletableFuture, Observable, Flowable, Flux - 推模型。當訂閱一個 pipeline ,並且某些事件被執行後,你會得到通知。推模型通常和非阻塞、非同步這些詞關聯在一起。當 pipeline 在某個執行緒上執行時,你可以做任何事情。你已經定義了一段待執行的程式碼,當通知到達的時候,這段程式碼就會在下個階段被執行。

7. Backpressure(回壓)

支援回壓的前提是 pipeline 必須是推模型。

Backpressure(回壓) 描述了 pipeline 中的一種場景:某些非同步階段的處理速度跟不上,需要告訴上游生產者放慢速度。直接失敗是不能接受的,這會導致大量資料的丟失。

backpressure.jpg

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

8. Operator fusion(操作融合)

操作融合的內涵在於,它使得生命週期的不同點上的執行階段得以改變,從而消除類庫的架構因素所造成的系統開銷。所有這些優化都在內部被處理完畢,從而讓外部使用者覺得這一切都是透明的。

只有 RxJava 2 和 Reactor 支援這個特性,但支援的方式不同。總的來說,有兩種型別的優化:

Macro-fusion - 用一個操作替換 2 個或更多的相繼的操作

macro-fusion_.png

Micro-fusion - 一個輸出佇列的結束操作,和在一個輸入佇列的開始操作,能夠共享一個佇列的例項。比如說,與其呼叫 request(1) 然後處理 onNext()`:

micro-fusion-1_1.png

不然讓訂閱者直接從父 observable 拉取值。

micro-fusion-2.png

更多資訊可以參考 Part1Part2

總結

一圖勝千言

2018-04-12_20-38-07.png

StreamCompletableFutureOptional 這些類的建立,都是為了解決特定的問題。 並且他們非常適合用於解決這些問題。 如果它們滿足你的需求,你可以立馬使用它們。

然而,不同的問題具有不同的複雜度,並且某些問題只有新技術才能很好的解決,新技術的出現也是為了解決那些高複雜度的問題。 RxJava 和 Reactor 是通用的工具,它們幫助你以宣告方式來解決問題,而不是使用那些不夠專業的工具,生搬硬套的使用其他的工具來解決響應式程式設計的問題,只會讓你的解決方案變成一種 hack 行為。

歡迎關注我的微信公眾號:「Kirito的技術分享」,關於文章的任何疑問都會得到回覆,帶來更多 Java 相關的技術分享。

關注微信公眾號

相關文章