Java 8 Stream(2)-原理解析

1. Strem 組件結構

1.1 操作分類

根據Java 8 Stream(1)-流的使用可以知道,Stream的操作可以分爲兩大類:中間操作與終結操作。中間操作只是對操作進行了記錄,終結操作纔會實際觸發計算邏輯(即惰性求值),源碼中也把 Stream 的一個操作稱爲一個 stage

  • 中間操作
    又可以分爲無狀態(Stateless)操作有狀態(Stateful)操作,前者是指元素的處理不受之前元素的影響,可以一個一個即時處理;後者是指該操作只有拿到所有元素之後才能繼續下去,比如排序是有狀態操作,在讀取所有元素之前並不能確定排序結果
  • 終結操作
    又可以分爲短路操作非短路操作,前者是指遇到某些符合條件的元素就可以得到最終結果;而後者是指必須處理所有元素才能得到最終結果

在這裏插入圖片描述

以下繼承結構以 ReferencePipeline 爲例,其對應的 Stream 類型爲 StreamShape.REFERENCE,也就是處理引用類型數據的流

在這裏插入圖片描述

1.2 操作對象的結構

   target.stream()
                .filter(fund -> fund.getConfirm()> 2)
                .map(fund->String.valueOf(fund.getMode()))
                .forEach(System.out::println);

以上代碼其對應的操作執行如下圖所示,其主要分爲兩個步驟:

  1. Stream 被構造處理的時候首先獲得一個 Head 對象,這是整個流操作執行鏈的頭節點。它也是一個操作行爲的封裝,只不過比較特殊,深度depth = 0,且沒有對數據的操作邏輯,其主要的作用是串起整個流處理流程。當中間操作都執行完,則獲得了一條對每一步操作都進行了描述的 Stream 中間操作雙向鏈表,頭指針指到了 stage2
  2. 當終結操作觸發時,以終結操作本身的數據處理邏輯的封裝對象 Sink0 爲起點,從操作鏈表尾部 stage2 逆向遍歷,將操作動作中封裝的數據處理邏輯封裝成 ChaineReference 對象,並將傳入的上一個 Sink 引用賦值給新建 Sink 的 downStream 變量,從而形成單向的調用鏈,頭指針指到了 Sink2

在這裏插入圖片描述

1.2.1 流中間操作鏈表頭對象 Head

ReferencePipeline.Head 內部對象爲例,追溯其構造方法,最終抵達 AbstractPipeline 的構造方法。這其實就是 Stream 中間操作對應的抽象節點,每一箇中間操作都被封裝成這樣一個節點,然後通過前後指針連接起來,形成了一條雙向鏈表。 從代碼看其比較重要的屬性如下:

  1. previousStage 當前中間操作節點的上一個節點,因爲 Head 爲整個雙向鏈表最上游,故其前一個節點爲 null
  2. sourceSpliterator 數據源的可分解迭代器,並行流中分解任務所需
  3. sourceStage 保存的 Head 頭節點引用,用於獲取保存在頭節點關於整個 Stream 處理流程中的關鍵信息,如是否是並行模式
  4. depth 當前節點的深度,Head 頭節點深度爲 0,該值在並行流大任務fork()分解子任務時可用於維護任務層級
  5. parallel 是否是並行模式,決定了是否啓用 ForkJoinPool 用於並行執行任務
  AbstractPipeline(Spliterator<?> source,
                     int sourceFlags, boolean parallel) {
        this.previousStage = null;
        this.sourceSpliterator = source;
        this.sourceStage = this;
        this.sourceOrOpFlags = sourceFlags & StreamOpFlag.STREAM_MASK;
        // The following is an optimization of:
        // StreamOpFlag.combineOpFlags(sourceOrOpFlags, StreamOpFlag.INITIAL_OPS_VALUE);
        this.combinedFlags = (~(sourceOrOpFlags << 1)) & StreamOpFlag.INITIAL_OPS_VALUE;
        this.depth = 0;
        this.parallel = parallel;
    }

1.2.2 流中間操作的其他對象

ReferencePipeline#map()對應的中間操作爲例,其代碼如下。可以看到其主要邏輯是生成一個新的 StatelessOp 對象,並且重寫了opWrapSink()方法

   public final <R> Stream<R> map(Function<? super P_OUT, ? extends R> mapper) {
        Objects.requireNonNull(mapper);
        return new StatelessOp<P_OUT, R>(this, StreamShape.REFERENCE,
                                     StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) {
            @Override
            Sink<P_OUT> opWrapSink(int flags, Sink<R> sink) {
                return new Sink.ChainedReference<P_OUT, R>(sink) {
                    @Override
                    public void accept(P_OUT u) {
                        downstream.accept(mapper.apply(u));
                    }
                };
            }
        };
    }

StatelessOp 的構造方法也會追溯到 AbstractPipeline 的構造方法,簡單地說其主要完成以下幾件事:

  1. 將調用方對象自身作爲參數傳入,成爲這個新的操作對象的 previousStage,執行previousStage.nextStage = this 將前一個操作對象的後指針指向這個新創建的操作對象,形成雙向鏈表結構
  2. 如果 Stream 中存在有狀態的中間操作,sourceStage.sourceAnyStateful = true 將其保存在 Head 頭節點中
  3. this.depth = previousStage.depth + 1 新創建的操作對象深度爲 previousStage 的 depth 加 1
    AbstractPipeline(AbstractPipeline<?, E_IN, ?> previousStage, int opFlags) {
        if (previousStage.linkedOrConsumed)
            throw new IllegalStateException(MSG_STREAM_LINKED);
        previousStage.linkedOrConsumed = true;
        previousStage.nextStage = this;

        this.previousStage = previousStage;
        this.sourceOrOpFlags = opFlags & StreamOpFlag.OP_MASK;
        this.combinedFlags = StreamOpFlag.combineOpFlags(opFlags, previousStage.combinedFlags);
        this.sourceStage = previousStage.sourceStage;
        if (opIsStateful())
            sourceStage.sourceAnyStateful = true;
        this.depth = previousStage.depth + 1;
    }

1.2.3 數據處理邏輯的封裝對象 Sink

ReferencePipeline#map()爲例,新的 StatelessOp 對象生成時重寫了opWrapSink()方法,該方法會返回一個 Sink 對象。我們已經知道了 Stream 操作是如何記錄下來形成雙向鏈表的,但是這些操作中封裝的回調方法(也就是我們寫的對數據源的處理邏輯)的真正執行卻需要藉助 SinkSink接口爲各個操作的數據處理邏輯的調用提供了規範,其包含的方法如下所示:

// 開始遍歷元素之前調用該方法,通知Sink做好準備
default void begin(long size)
// 所有元素遍歷完成之後調用,通知Sink沒有更多的元素了
default void end()
// 是否可以結束操作,可以讓短路操作儘早結束
default boolean cancellationRequested() 
// 遍歷元素時調用,接受一個待處理元素,並對元素進行處理
// Stage 把自己包含的回調方法封裝到該方法裏,前一個Stage只需要調用當前 Stage.accept(T t)方法就行了
default void accept(int value)

有了規範,每個操作對象都將自己的數據處理邏輯封裝到一個Sink中,前一個操作只需調用後一個操作的Sink接口方法即可,不需要知道其內部是如何處理的。事實上 Stream 各個操作內部實現的本質,就是如何重載 Sink 的四個接口方法,各操作實現自己的邏輯,處理數據時只需要從 Head 開始對數據源依次調用每個操作對象對應的Sink.{begin(),accept(),cancellationRequested(),end()}方法就可以了

  • 對於有狀態的操作,Sinkbegin()end()方法是必須實現的。例如Stream.sorted(),其操作對象封裝的數據處理邏輯 RefSortingSink 對象中,begin()方法創建了一個存儲元素的容器,accept()方法負責將元素添加到該容器,最後end()實現對容器中的元素進行排序,並決定了下游 Sink 如何執行
  • 對於短路操作,Sink 的 cancellationRequested()方法也是必須實現的,比如Stream.findFirst()是短路操作,只要找到一個元素cancellationRequested()就要返回true,以便儘快結束查找

Sink 的繼承結構如下,其中 BooleanTerminalSinkMatch 操作的數據處理邏輯抽象,AccumulationgSinkReduce 操作的數據處理邏輯抽象,ChainedReference所有中間操作的數據處理邏輯抽象
在這裏插入圖片描述

2. Stream 實現原理

2.1 Spliterator 可分割迭代器

Spliterator可以看作一個splittable Iterator, 是 Java 8 中新增的一個迭代器。其缺省方法forEachRemaining(Consumer<? super E> action)內部是一個循環,tryAdvance()方法對下一個未處理的元素執行 action 並返回 true, 如果沒有下一個元素返回 false。另外它也提供了trySplit(),實現數據源的拆解,爲多線程並行處理提供最小任務

    /**
     * Performs the given action for each remaining element, sequentially in
     * the current thread, until all elements have been processed or the action
     * throws an exception.  If this Spliterator is {@link #ORDERED}, actions
     * are performed in encounter order.  Exceptions thrown by the action
     * are relayed to the caller.
     *
     * @implSpec
     * The default implementation repeatedly invokes {@link #tryAdvance} until
     * it returns {@code false}.  It should be overridden whenever possible.
     *
     * @param action The action
     * @throws NullPointerException if the specified action is null
     */
    default void forEachRemaining(Consumer<? super T> action) {
        do { } while (tryAdvance(action));
    }

2.2 Stream 串行處理流程

在這裏插入圖片描述

  1. ArrayList#stream() 方法作爲流處理的入口,從代碼看其實是調用了 StreamSupport#stream() 方法,從而生成了操作記錄鏈表的頭節點 ReferencePipeline.Head,並將其引用返回

    public static <T> Stream<T> stream(Spliterator<T> spliterator, boolean parallel) {
         Objects.requireNonNull(spliterator);
         return new ReferencePipeline.Head<>(spliterator,
                                             StreamOpFlag.fromCharacteristics(spliterator),
                                             parallel);
     }
    
  2. 外部持有了 ReferencePipeline.Head 引用,再調用 filter() 方法其實是調用到了ReferencePipeline#filter(),此時會新建一個無狀態的中間操作,其重寫的 opWrapSink() 規定了該操作的下游操作的Sink是如何組織數據處理邏輯的。完成這些後將新建的中間操作引用返回,之後再調用 map() 方法流程與此類似

    @Override
     public final Stream<P_OUT> filter(Predicate<? super P_OUT> predicate) {
         Objects.requireNonNull(predicate);
         return new StatelessOp<P_OUT, P_OUT>(this, StreamShape.REFERENCE,
                                      StreamOpFlag.NOT_SIZED) {
             @Override
             Sink<P_OUT> opWrapSink(int flags, Sink<P_OUT> sink) {
                 return new Sink.ChainedReference<P_OUT, P_OUT>(sink) {
                     @Override
                     public void begin(long size) {
                         downstream.begin(-1);
                     }
    
                     @Override
                     public void accept(P_OUT u) {
                         if (predicate.test(u))
                             downstream.accept(u);
                     }
                 };
             }
         };
     }
    
  3. 當終結方法 forEach() 被調用時,首先調用 ForEachOps#makeRef() 新建最終操作 ForEachOp 對象,之後調用了 AbstractPipeline#evaluate() 開始執行操作中定義的數據處理邏輯

    @Override
     public void forEach(Consumer<? super P_OUT> action) {
         evaluate(ForEachOps.makeRef(action, false));
     }
    
  4. AbstractPipeline#evaluate() 需要判斷當前流是否是並行流,此處以串行流分析,則調用了 terminalOp.evaluateSequential()

    final <R> R evaluate(TerminalOp<E_OUT, R> terminalOp) {
         assert getOutputShape() == terminalOp.inputShape();
         if (linkedOrConsumed)
             throw new IllegalStateException(MSG_STREAM_LINKED);
         linkedOrConsumed = true;
    
         return isParallel()
                ? terminalOp.evaluateParallel(this, sourceSpliterator(terminalOp.getOpFlags()))
                : terminalOp.evaluateSequential(this, sourceSpliterator(terminalOp.getOpFlags()));
     }
    
  5. terminalOp.evaluateSequential() 方法調用到重寫方法 ForEachOp#evaluateSequential(),可以看到其內部邏輯很清晰,其實就是調用到了 AbstractPipeline#wrapAndCopyInto()

     @Override
         public <S> Void evaluateSequential(PipelineHelper<T> helper,
                                            Spliterator<S> spliterator) {
             return helper.wrapAndCopyInto(this, spliterator).get();
         }
    
  6. AbstractPipeline#wrapAndCopyInto() 中包含了整個流中最重要的邏輯,其主要是分爲了兩個步驟:

    1. wrapSink() 從操作鏈表的尾部開始,調用操作對象自身重寫的 opWrapSink()方法將每一個操作對象中的數據處理邏輯封裝成 Sink.ChainedReference,並將傳入的 Sink 作爲新建 Sink 的 downStream,從而形成單向調用鏈
    2. copyInto()從調用鏈頭部開始執行中間操作數據處理邏輯封裝成的 Sink 對象的方法,完成對數據源的處理
    @Override
    final <P_IN, S extends Sink<E_OUT>> S wrapAndCopyInto(S sink, Spliterator<P_IN> spliterator) {
        copyInto(wrapSink(Objects.requireNonNull(sink)), spliterator);
        return sink;
    }
    
  7. AbstractPipeline#wrapSink() 是數據處理邏輯從中間操作中抽取出來封裝成 Sink 對象的關鍵步驟,其主要邏輯是回調中間操作對象的 opWrapSink() 方法,將其中包含的數據處理邏輯封裝爲新的 Sink ,並將當前的 Sink 作爲新建 SinkdownStream,促使單向調用鏈成型

    final <P_IN> Sink<P_IN> wrapSink(Sink<E_OUT> sink) {
        Objects.requireNonNull(sink);
    
        for ( @SuppressWarnings("rawtypes") AbstractPipeline p=AbstractPipeline.this; p.depth > 0; p=p.previousStage) {
            sink = p.opWrapSink(p.previousStage.combinedFlags, sink);
        }
        return (Sink<P_IN>) sink;
    }
    
  8. AbstractPipeline#copyInto()方法主要負責調用 Sink 鏈, spliterator.forEachRemaining()藉助迭代器對每一個未被遍歷的元素應用對數據的處理邏輯

    final <P_IN> void copyInto(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator) {
         Objects.requireNonNull(wrappedSink);
    
         if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) {
             wrappedSink.begin(spliterator.getExactSizeIfKnown());
             spliterator.forEachRemaining(wrappedSink);
             wrappedSink.end();
         }
         else {
             copyIntoWithCancel(wrappedSink, spliterator);
         }
     }
    
  9. ArrayListSpliterator#forEachRemaining() 中,action.accept() 在當前 Sink#accept() 方法被調用後,會調用下一個 Sink 的相關方法完成對數據的流處理

    public void forEachRemaining(Consumer<? super E> action) {
             int i, hi, mc; // hoist accesses and checks from loop
             ArrayList<E> lst; Object[] a;
             if (action == null)
                 throw new NullPointerException();
             if ((lst = list) != null && (a = lst.elementData) != null) {
                 if ((hi = fence) < 0) {
                     mc = lst.modCount;
                     hi = lst.size;
                 }
                 else
                     mc = expectedModCount;
                 if ((i = index) >= 0 && (index = hi) <= a.length) {
                     for (; i < hi; ++i) {
                         @SuppressWarnings("unchecked") E e = (E) a[i];
                         action.accept(e);
                     }
                     if (lst.modCount == mc)
                         return;
                 }
             }
             throw new ConcurrentModificationException();
         }
    

2.3 Stream 並行處理流程

在這裏插入圖片描述

  1. 流的並行處理與串行處理差別不是很大,主要的差異在於當終結操作被觸發時,其調用的是terminalOp.evaluateParallel(),最終調用到其重寫方法 ForEachOp#evaluateParallel(),然後生成了 ForEachTask 這個 ForkJoinTask的子類,並調用其 invoke()提交到線程池執行

    需注意該步驟中的 helper.wrapSink(this)會在生成線程池任務的時候生成 Sink 調用鏈

    @Override
         public <S> Void evaluateParallel(PipelineHelper<T> helper,
                                          Spliterator<S> spliterator) {
             if (ordered)
                 new ForEachOrderedTask<>(helper, spliterator, this).invoke();
             else
                 new ForEachTask<>(helper, spliterator, helper.wrapSink(this)).invoke();
             return null;
         }
    
  2. ForkJoinPool 相關流程請參考 Java 線程池源碼詳解(2)-ForkJoinPool 源碼解析,可知最後會調用到 ForEachTask#compute() 方法。這個方法內部會開啓 while 循環對任務進行估算,然後fork()分解大任務,直到任務足夠小才執行 task.helper.copyInto(taskSink, rightSplit),之後的數據處理流程與串行流完全一致

    // Similar to AbstractTask but doesn't need to track child tasks
         public void compute() {
             Spliterator<S> rightSplit = spliterator, leftSplit;
             long sizeEstimate = rightSplit.estimateSize(), sizeThreshold;
             if ((sizeThreshold = targetSize) == 0L)
                 targetSize = sizeThreshold = AbstractTask.suggestTargetSize(sizeEstimate);
             boolean isShortCircuit = StreamOpFlag.SHORT_CIRCUIT.isKnown(helper.getStreamAndOpFlags());
             boolean forkRight = false;
             Sink<S> taskSink = sink;
             ForEachTask<S, T> task = this;
             while (!isShortCircuit || !taskSink.cancellationRequested()) {
                 if (sizeEstimate <= sizeThreshold ||
                     (leftSplit = rightSplit.trySplit()) == null) {
                     task.helper.copyInto(taskSink, rightSplit);
                     break;
                 }
                 ForEachTask<S, T> leftTask = new ForEachTask<>(task, leftSplit);
                 task.addToPendingCount(1);
                 ForEachTask<S, T> taskToFork;
                 if (forkRight) {
                     forkRight = false;
                     rightSplit = leftSplit;
                     taskToFork = task;
                     task = leftTask;
                 }
                 else {
                     forkRight = true;
                     taskToFork = leftTask;
                 }
                 taskToFork.fork();
                 sizeEstimate = rightSplit.estimateSize();
             }
             task.spliterator = null;
             task.propagateCompletion();
         }
    

2.3.1 Stream 並行流中線程池任務的繼承結構

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章