JDK 9新特性之Flow API 初探

北冥有隻魚發表於2022-06-05
本身我是隻打算介紹JDK 11的 新的Http Client的,但是又碰見Flow API 響應式流,只好將這部分東西獨立出來,簡單介紹一下。

[TOC]

響應式流的引入

Reactive Stream 反應式流或響應式流,這個詞我是在介紹JDK 11中的HttpClient中碰到的:

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://openjdk.java.net/"))
.POST(HttpRequest.BodyPublishers.ofString("aaaa"))
                .build();
// BodyHandlers.fromLineSubscriber要求的引數是Subscriber類例項
// 然後我點了點發現Subscriber位於Flow類內是一個靜態介面
client.sendAsync(request, HttpResponse.BodyHandlers.fromLineSubscriber())

JDk

往上翻了一下發現這個Flow出自Doug Lea大佬之手,上面還寫了Since 9,也就是說這個類是在JDK 9之後才進入到JDK裡面的。

Doug Lea

Doug Lea的註釋一向是註釋比程式碼多,我們先看註釋看,看看引入這個Flow 類是為了什麼?

Interrelated interfaces and static methods for establishing flow-controlled components in which {@link Publisher Publishers} produce items consumed by one or more {@link Subscriber Subscribers}, each managed by a {@link Subscription Subscription}.
這些介面和靜態方法都是為了建立一起釋出-訂閱者模式(Publisher釋出者釋出 一個或多個Subscriber訂閱者消費,每個訂閱者被Subscription管理)的流式控制元件。

<p>These interfaces correspond to the reactive-streams
specification. They apply in both concurrent and distributed
asynchronous settings: All (seven) methods are defined in {@code
void} "one-way" message style. Communication relies on a simple form
of flow control (method {@link Subscription#request}) that can be
used to avoid resource management problems that may otherwise occur
in "push" based systems.
這些介面遵循響應式流的規範,他們被應用於併發和分散式非同步設定: 所有七個方法都被定義為返回值為void的單向訊息風格。
訊息的交流依賴於簡單的流式控制(Subscription的request方法)可以用來避免基於推送系統的一些資源管理問題。

這個響應流規範是啥? 我開啟了href的這個連結進行檢視。

為什麼要引入響應流規範

Reactive Streams is an initiative to provide a standard for asynchronous stream processing with non-blocking back pressure. This encompasses efforts aimed at runtime environments (JVM and JavaScript) as well as network protocols.
響應流式一種倡議,旨在為具有非阻塞背壓的非同步流處理提供標準,這包括針對JVM執行時環境、javaScript、網路協議的工作。

Handling streams of data—especially “live” data whose volume is not predetermined—requires special care in an asynchronous system. The most prominent issue is that resource consumption needs to be controlled such that a fast data source does not overwhelm the stream destination. Asynchrony is needed in order to enable the parallel use of computing resources, on collaborating network hosts or multiple CPU cores within a single machine.

在非同步系統中處理,處理資料流,尤其是資料量未預先確定的實施資料要特別小心。最為突出而又常見的問題是資源消費控制的問題,以便防止大量資料快速到來淹沒目的地。
為了讓讓一片網路的計算機或者一臺計算機內的多核CPU在執行計算任務的時候使用並行模式,我們需要非同步。

The main goal of Reactive Streams is to govern the exchange of stream data across an asynchronous boundary—think passing elements on to another thread or thread-pool—while ensuring that the receiving side is not forced to buffer arbitrary amounts of data. In other words, back pressure is an integral part of this model in order to allow the queues which mediate between threads to be bounded. The benefits of asynchronous processing would be negated if the communication of back pressure were synchronous (see also the Reactive Manifesto), therefore care has to be taken to mandate fully non-blocking and asynchronous behavior of all aspects of a Reactive Streams implementation.

響應流的主要目標是控制跨越非同步邊界的資料交換,即將一個元素傳遞到令外一個執行緒或執行緒中要確保接收方不會被迫緩衝任意數量的資料。換句話說,背壓是該模型的重要組成,該模型可以讓執行緒之間的佇列是有界的。如果採取背壓方式的通訊是同步的,那麼非同步處理的方式將會被否定的(詳見響應式宣言)。因此必須要求所有的反應式流實現都是非同步和非阻塞的。
It is the intention of this specification to allow the creation of many conforming implementations, which by virtue of abiding by the rules will be able to interoperate smoothly, preserving the aforementioned benefits and characteristics across the whole processing graph of a stream application.
遵守本規範的實現可以實現互動操作,從而在整個流應用的處理過程中受益。

JDK 9 的正式釋出時間是2017年9月, 如果你點搜尋Reactive Manifesto,會發現這個宣言於14年9月16日釋出,這是一種程式設計理念,對應的有響應式程式設計,同物件導向程式設計、函數語言程式設計一樣,是一種理念。推出規範就是為了約束其實現,避免每個庫有有自己的一套響應式實現,這對於開發者來說是一件很頭痛的事情。響應式程式設計的提出如上文所示主要是為了解決非同步資料處理的背壓現象,那什麼是背壓。

背壓的解釋

背壓並不是響應式程式設計獨有的概念,背壓的英文是BackPressure,不是一種機制,也不是一種策略,而是一種現象: 在資料流從上游生產者向下遊消費者傳輸的過程中,上游生產速度大於下游消費速度,導致下游的Buffer溢位,這種現象我們稱之為Backpressure出現。背壓的重點在於上游的生產速度大於下游消費速度,而在於Buffer溢位。

舉一個例子就是在Java中,我們向執行緒池中提交任務,佇列滿了觸發拒絕策略(拒絕接受新任務還是丟棄舊的處理新的)。寫到這裡可能有同學會說,那你用無界佇列不行嗎?那如果提交的任務不斷膨脹,導致你整個系統崩潰掉了怎麼辦? 如果上游系統生產速度快到可以把系統搞崩潰,那麼就需要設定Buffer上限。

梳理一下

首先出現響應式程式設計理念,然後出現響應式程式設計實現,再然後出現響應式規範,響應流主要解決處理元素流的問題—如何將元素流從釋出者傳遞到訂閱者,不而不需要釋出者阻塞,或者要求訂閱者有無限的緩衝區,有限的緩衝區在到達緩衝上界的時候,對到達的元素進行丟棄或者拒絕,訂閱者可以非同步通知釋出者降低或提升資料生產釋出的速率,它是響應式程式設計實現效果的核心特點。

發展歷程

而響應式規範則是一種倡議,遵循此倡議的系統可以讓資料在各個響應式系統中都實現響應式的處理資料,規範在Java中的形式就是介面,也就是我們本篇的主題Flow 類,對於一項標準而言,它的目的自然是用更少的協議來描述互動。而響應流模型也非常簡單:

  • 訂閱者非同步的向釋出者請求N個元素
  • 釋出者非同步的向訂閱者傳送( 0 < M <= N)個元素。

寫到這裡可能有同學會問了,為啥不是訂閱者要多少元素,釋出者給多少啊? 這其實上是一種協調機制, 在消費資料中有以下兩種情況值得我們注意:

  • 訂閱者消費過快(在響應式模型中, 處理這種情況是通知釋出者產生元素快一點,類似於去包子店吃包子, 飯量比較大的顧客來,包子店生產不及,就會告訴包子店做的快一點,說完還接著吃包子)

push模型

  • 釋出者釋出過快(在響應式模型中,處理這種情況是通知生產者降低生產速率,還是去包子店吃包子,雖然顧客飯量比較大,但是吃的比較慢,很快擺不下了,就會告訴包子店做的慢一些)

pull模型

Flow的大致介紹

Flow是一個被final關鍵字修飾的類,裡面是幾組public static介面和buffer變數長度:

  • Publisher 釋出者
  • Subscriber 訂閱者
  • Subscription 訂閱信件(或訂閱令牌), 通過此例項, 用於訂閱者和釋出者之間協調請求元素數量和請求訂閱元素數量
  • Processor 繼承Publisher 和 Subscriber,用於連線Publisher和Subscriber, 也可以連線其他處理器

響應式流程

簡單示例

public class FlowDemo {
    static class SampleSubscriber<T> implements Flow.Subscriber<T> {
        final Consumer<? super T> consumer;
        Flow.Subscription subscription;
        SampleSubscriber(Consumer<? super T> consumer) {
            this.bufferSize = bufferSize;
            this.consumer = consumer;
        }
        @Override
        public void onSubscribe(Flow.Subscription subscription) {
            System.out.println("建立訂閱關係");
            this.subscription = subscription; // 賦值
            subscription.request(2);
        }
        public void onNext(T item) {
            System.out.println("收到傳送者的訊息"+ item);
            consumer.accept(item);
            // 可呼叫 subscription.request 接著請求釋出者發訊息
          //  subscription.request(1);
        }
        public void onError(Throwable ex) { ex.printStackTrace(); }
        public void onComplete() {}
    }

    public static void main(String[] args) {
        SampleSubscriber subscriber = new SampleSubscriber<>(200L,o->{
            System.out.println("hello ....." + o);
        });
        ExecutorService executor = Executors.newFixedThreadPool(1);
        SubmissionPublisher<Boolean> submissionPublisher = new SubmissionPublisher(executor,Flow.defaultBufferSize());
        submissionPublisher.subscribe(subscriber);
        submissionPublisher.submit(true);
        submissionPublisher.submit(true);
        submissionPublisher.submit(true);
        executor.shutdown();
    }
}

輸出結果:

結果示例

為啥釋出者釋出了三條訊息,你訂閱者只處理了兩條啊,因為在建立訂閱關係的時候訂閱者就跟釋出者說明了, 我只要兩條訊息, 當前消費能力不足, 在消費之後, 還可以再請求釋出者傳送。

下面我們來演示一下背壓效果, 我們現在設定緩衝池大小的任務是Flow定義的預設值, 256。 我們現在嘗試提交1000個任務試試看:

public class FlowDemo {
    static class SampleSubscriber<T> implements Flow.Subscriber<T> {
        final Consumer<? super T> consumer;
        Flow.Subscription subscription;
        SampleSubscriber(Consumer<? super T> consumer) {
            this.consumer = consumer;
        }
        @Override
        public void onSubscribe(Flow.Subscription subscription) {
            System.out.println("建立訂閱關係");
            this.subscription = subscription; // 賦值
            subscription.request(1);
        }
        public void onNext(T item) {
            try {
                System.out.println("thread name 0"+Thread.currentThread().getName());
                TimeUnit.SECONDS.sleep(30);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("收到傳送者的訊息"+ item);
            consumer.accept(item);
            // 可呼叫 subscription.request 接著請求釋出者發訊息
            subscription.request(1);
        }
        public void onError(Throwable ex) { ex.printStackTrace(); }
        public void onComplete() {}
    }

    public static void main(String[] args) {
        SampleSubscriber subscriber = new SampleSubscriber<>(o->{
            System.out.println("hello ....." + o);
        });
        ExecutorService executor = Executors.newFixedThreadPool(1);
        SubmissionPublisher<Boolean> submissionPublisher = new SubmissionPublisher(executor,Flow.defaultBufferSize());
        submissionPublisher.subscribe(subscriber);
        for (int i = 0; i < 1000; i++) {
            System.out.println("開始釋出第"+i+"條訊息");
            submissionPublisher.submit(true);
            System.out.println("開始釋出第"+i+"條訊息釋出完畢");
        }
        executor.shutdown();
    }
}

緩衝區大小示例

為什麼到第257條被阻塞住了, 那是因為緩衝區滿了, 緩衝區出現空閒才會被允許接著生產。

public class MyProcessor extends SubmissionPublisher<Boolean> implements Flow.Processor<Boolean, Boolean> {
    private Flow.Subscription subscription;

    @Override
    public void onSubscribe(Flow.Subscription subscription) {
        this.subscription = subscription;
        this.subscription.request(1);
    }

    @Override
    public void onNext(Boolean item) {
        if (item){
            item = false;
            // 處理器將此條資訊轉發
            this.submit(item);
            System.out.println("將true 轉換為false");
        }
        subscription.request(1);
    }

    @Override
    public void onError(Throwable throwable) {
        throwable.printStackTrace();
        this.subscription.cancel();
    }

    @Override
    public void onComplete() {
        System.out.println("處理器處理完畢");
        this.close();
    }
}
public class FlowDemo {
    static class SampleSubscriber<T> implements Flow.Subscriber<T> {
        final Consumer<? super T> consumer;
        Flow.Subscription subscription;
        SampleSubscriber(Consumer<? super T> consumer) {
            this.consumer = consumer;
        }
        @Override
        public void onSubscribe(Flow.Subscription subscription) {
            System.out.println("建立訂閱關係");
            this.subscription = subscription; // 賦值
            subscription.request(1);
        }
        public void onNext(T item) {
            System.out.println("收到傳送者的訊息"+ item);
            consumer.accept(item);
            // 可呼叫 subscription.request 接著請求釋出者發訊息
            subscription.request(1);
        }
        public void onError(Throwable ex) { ex.printStackTrace(); }
        public void onComplete() {}
    }

    public static void main(String[] args) throws Exception{
        SampleSubscriber subscriber = new SampleSubscriber<>(o->{
            System.out.println("hello ....." + o);
        });
        ExecutorService executor = Executors.newFixedThreadPool(1);
        SubmissionPublisher<Boolean> submissionPublisher = new SubmissionPublisher(executor,Flow.defaultBufferSize());
        MyProcessor myProcessor = new MyProcessor();
        // 做資訊轉發
        submissionPublisher.subscribe(myProcessor);
        myProcessor.subscribe(subscriber);
        for (int i = 0; i < 2; i++) {
            System.out.println("開始釋出第"+i+"條訊息");
            submissionPublisher.submit(true);
            System.out.println("開始釋出第"+i+"條訊息釋出完畢");
        }
        TimeUnit.SECONDS.sleep(2);
        executor.shutdown();
    }
}

輸出結果:

轉換成功

總結一下

我們由JDK 11的HTTP Client的請求引數看到了Flow API, 在Flow類中的註釋中看到了Reactive Stream, 由Reactive Stream看到了響應式規範, 由規範引出響應流解決的問題, 即協調發布者和訂閱者,釋出者釋出太快, 訂閱者請求釋出者減緩生產速度,生產太慢,訂閱者請求釋出者加快速度。在Java領域已經有了響應式的一些實現:

  • RXJava 是ReactiveX專案中的Java實現,Rxjava早於Reactive Streams規範, RXJava 2.0+確實實現了Reactive Streams API規範。
  • Reactor是Pivotal提供的Java實現,它作為Spring Framework 5的重要組成部分,是WebFlux採用的預設反應式框架
  • Akka Streams完全實現了Reactive Streams規範,但Akka Streams API與Reactive Streams API完全分離。

為了統一規範,JDK 9引入了Flow,Flow可以類似於JDBC, 屬於API規範,實際使用時需要使用API對應的具體實現,Reactive Streams為我們提供了一個我們可以程式碼的介面,而無需關心其實現。

參考資料

相關文章