解構反應式程式設計——Java8,RxJava,Reactor之比較

jacobwu發表於2018-05-10

如果你熟悉Java 8,同時又瞭解反應式程式設計(Reactive Programming)框架,例如RxJava和Reactor等,你可能會問:

“如果我可以用Java 8 的Stream, CompletableFuture, 以及Optional完成同樣的事情,為什麼還要用RxJava 或者 Reactor呢?”

問題在於,大多數時候你在處理的是簡單的任務,這個時候你確實不需要那些反應式程式設計的庫。但是,當系統越來越複雜,或者你處理的本身就是個複雜的任務,你恐怕就得寫一些讓自己頭皮發麻的程式碼。隨著時間的推移,這些程式碼會變得越來越複雜和難以維護。

RxJava和Reactor提供了很多非常趁手的功能,能夠支援你在未來更輕鬆地維護你的程式碼,實現新需求。但是這個優勢到底有多大,具體體現在哪些方面?沒有標準無法比較,讓我們定義8個比較的維度,來幫助我們理解Java 8的API以及反應式程式設計的庫之間的差別。

  1. Composable(可組裝)
  2. Lazy(延遲執行)
  3. Reusable(可重用)
  4. Asynchronous(非同步)
  5. Cacheable(可快取)
  6. Push or Pull(推還是拉)
  7. Backpressure(反壓)
  8. Operator fusion(操作融合)

針對上面這些維度,我們比較以下的這些類:

  1. CompletableFuture
  2. Stream
  3. Optional
  4. Observable (RxJava 1)
  5. Observable (RxJava 2)
  6. Flowable (RxJava 2)
  7. 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(反壓) 描述的是在流水線中會發生的一種場景:某些非同步的階段處理速度跟不上,需要告訴上游生產者放慢速度。直接失敗是不可接受的,因為會丟失太多資料。

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。

Operator fusion(操作融合)

操作融合背後的想法是,在生命週期的不同點上,改變執行階段的鏈條,從而消除庫的架構因素所造成的額外開銷。所有這些優化都是在內部處理掉的,對外部使用者來說是透明的。

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

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

macro-fusion_.png

  • Micro-fusion – 在一個輸出佇列中結束的操作,和在一個前驅佇列中開始的操作,能夠共用同一個佇列的例項。比如說,與其呼叫request(1)然後處理onOnext(),我們可以:

micro-fusion-1_1.png

訂閱者可以向父observable輪詢值。

micro-fusion-2.png

更多的詳細資訊可以參考Operator-fusion (part 1)Operator-fusion (part 2)

總結

上面的內容可以總結為一個表:

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

總的來說,Stream, CompletableFuture, 和 Optional建立出來是為了解決特定的問題。它們解決這些問題很好用。如果它們滿足你的要求,你繼續用它們就好了。

但是,不同的問題有不同的複雜性。某些問題需要新的技術。RxJava 和 Reactor是一組通用的工具,幫助你用一種宣告式的方式解決你面對的問題,而不是用一些並非為這種問題而提供的工具,來建立一種“hack”的解決方案。

本文主要內容翻譯自:http://alexsderkach.io/comparing-java-8-rxjava-reactor/


相關文章