美文网首页
Stream流水线原理

Stream流水线原理

作者: 王侦 | 来源:发表于2019-07-24 21:32 被阅读0次

    1.无状态、有状态操作及短路、非短路操作

    Stream上的所有操作分为两类:中间操作和终止操作。中间操作只是一种标记,只有终止操作才会触发实际计算。


    中间操作又可以分为无状态的(Stateless)和有状态的(Stateful):

    • 无状态中间操作是指元素的处理不受前面元素的影响
    • 有状态的中间操作必须等到所有元素处理之后才知道最终结果,比如排序是有状态操作,在读取所有元素之前并不能确定排序结果,

    结束操作又可以分为短路操作和非短路操作:

    • 短路操作是指不用处理全部元素就可以返回结果,比如找到第一个满足条件的元素。

    之所以要进行如此精细的划分,是因为底层对每一种情况的处理方式不同。

    2.迭代实现

            OptionalInt longestStringLengthStartingWithA
                    = strings.stream()
                    .filter(s -> s.startsWith("A"))
                    .mapToInt(String::length)
                    .max();
    

    这种实现的两个弊端:

    • 迭代次数多。迭代次数跟函数调用的次数相等。
    • 频繁产生中间结果。每次函数调用都产生一次中间结果,存储开销无法接受。

    使用一次迭代实现:

    int longest = 0;
    for(String str : strings){
        if(str.startsWith("A")){ // 1. filter()
            int len = str.length(); // 2. mapToInt()
            longest = Math.max(len, longest); // 3. max()
        }
    }
    

    3.流水线怎样实现尽可能减少迭代次数?

    应该采用某种方式记录用户每一步的操作,当用户调用结束操作时将之前记录的操作叠加到一起在一次迭代中全部执行掉。有几个问题需要解决:

    • 用户的操作如何记录?
    • 操作如何叠加?
    • 叠加之后的操作如何执行?
    • 执行后的结果(如果有)在哪里?

    3.1 操作如何记录

    指的是Stream中间操作,很多Stream操作会需要一个回调函数(Lambda表达式),因此一个完整的操作是<数据源,操作,回调函数>构成的三元组。Stream中使用Stage的概念来描述一个完整的操作,并用某种实例化后的PipelineHelper来代表Stage,将具有先后顺序的各个Stage连到一起,就构成了整个流水线。跟Stream相关类和接口的继承关系图示。


    IntPipeline, LongPipeline, DoublePipeline没三个类专门为三种基本类型(不是包装类型)而定制的,跟ReferencePipeline是并列关系。图中Head用于表示第一个Stage,即调用调用诸如Collection.stream()方法产生的Stage,很显然这个Stage里不包含任何操作;StatelessOp和StatefulOp分别表示无状态和有状态的Stage,对应于无状态和有状态的中间操作。

    通过Collection.stream()方法得到Head也就是stage0,紧接着调用一系列的中间操作,不断产生新的Stream。这些Stream对象以双向链表的形式组织在一起,构成整个流水线,由于每个Stage都记录了前一个Stage和本次的操作以及回调函数,依靠这种结构就能建立起对数据源的所有操作。这就是Stream记录操作的方式。

    3.2 操作如何叠加

    以上只是解决了操作记录的问题,要想让流水线起到应有的作用我们需要一种将所有操作叠加到一起的方案。你可能会觉得这很简单,只需要从流水线的head开始依次执行每一步的操作(包括回调函数)就行了。这听起来似乎是可行的,但是你忽略了前面的Stage并不知道后面Stage到底执行了哪种操作,以及回调函数是哪种形式。换句话说,只有当前Stage本身才知道该如何执行自己包含的动作。这就需要有某种协议来协调相邻Stage之间的调用关系。

    这种协议由Sink接口完成,Sink接口包含的方法如下表所示:


    有了上面的协议,相邻Stage之间调用就很方便了,每个Stage都会将自己的操作封装到一个Sink里,前一个Stage只需调用后一个Stage的accept()方法即可,并不需要知道其内部是如何处理的。当然对于有状态的操作,Sink的begin()和end()方法也是必须实现的。比如Stream.sorted()是一个有状态的中间操作,其对应的Sink.begin()方法可能创建一个乘放结果的容器,而accept()方法负责将元素添加到该容器,最后end()负责对容器进行排序。对于短路操作,Sink.cancellationRequested()也是必须实现的,比如Stream.findFirst()是短路操作,只要找到一个元素,cancellationRequested()就应该返回true,以便调用者尽快结束查找。Sink的四个接口方法常常相互协作,共同完成计算任务。实际上Stream API内部实现的的本质,就是如何重载Sink的这四个接口方法。

    有了Sink对操作的包装,Stage之间的调用问题就解决了,执行时只需要从流水线的head开始对数据源依次调用每个Stage对应的Sink.{begin(), accept(), cancellationRequested(), end()}方法就可以了。一种可能的Sink.accept()方法流程是这样的:

    void accept(U u){
        1. 使用当前Sink包装的回调函数处理u
        2. 将处理结果传递给流水线下游的Sink
    }
    

    Sink接口的其他几个方法也是按照这种[处理->转发]的模型实现。下面我们结合具体例子看看Stream的中间操作是如何将自身的操作包装成Sink以及Sink是如何将处理结果转发给下一个Sink的。

    3.2.1 无状态的Stream.map()方法

    // Stream.map(),调用该方法将产生一个新的Stream
    public final <R> Stream<R> map(Function<? super P_OUT, ? extends R> mapper) {
        ...
        return new StatelessOp<P_OUT, R>(this, StreamShape.REFERENCE,
                                     StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) {
            @Override /*opWripSink()方法返回由回调函数包装而成Sink*/
            Sink<P_OUT> opWrapSink(int flags, Sink<R> downstream) {
                return new Sink.ChainedReference<P_OUT, R>(downstream) {
                    @Override
                    public void accept(P_OUT u) {
                        R r = mapper.apply(u);// 1. 使用当前Sink包装的回调函数mapper处理u
                        downstream.accept(r);// 2. 将处理结果传递给流水线下游的Sink
                    }
                };
            }
        };
    }
    
    

    上述代码看似复杂,其实逻辑很简单,就是将回调函数mapper包装到一个Sink当中。由于Stream.map()是一个无状态的中间操作,所以map()方法返回了一个StatelessOp内部类对象(一个新的Stream),调用这个新Stream的opWripSink()方法将得到一个包装了当前回调函数的Sink。

    3.2.2 有状态的Stream.sorted()方法

    Stream.sorted()方法将对Stream中的元素进行排序,显然这是一个有状态的中间操作,因为读取所有元素之前是没法得到最终顺序的。抛开模板代码直接进入问题本质,sorted()方法是如何将操作封装成Sink的呢?sorted()一种可能封装的Sink代码如下:

    // Stream.sort()方法用到的Sink实现
    class RefSortingSink<T> extends AbstractRefSortingSink<T> {
        private ArrayList<T> list;// 存放用于排序的元素
        RefSortingSink(Sink<? super T> downstream, Comparator<? super T> comparator) {
            super(downstream, comparator);
        }
        @Override
        public void begin(long size) {
            ...
            // 创建一个存放排序元素的列表
            list = (size >= 0) ? new ArrayList<T>((int) size) : new ArrayList<T>();
        }
        @Override
        public void end() {
            list.sort(comparator);// 只有元素全部接收之后才能开始排序
            downstream.begin(list.size());
            if (!cancellationWasRequested) {// 下游Sink不包含短路操作
                list.forEach(downstream::accept);// 2. 将处理结果传递给流水线下游的Sink
            }
            else {// 下游Sink包含短路操作
                for (T t : list) {// 每次都调用cancellationRequested()询问是否可以结束处理。
                    if (downstream.cancellationRequested()) break;
                    downstream.accept(t);// 2. 将处理结果传递给流水线下游的Sink
                }
            }
            downstream.end();
            list = null;
        }
        @Override
        public void accept(T t) {
            list.add(t);// 1. 使用当前Sink包装动作处理t,只是简单的将元素添加到中间列表当中
        }
    }
    

    上述代码完美的展现了Sink的四个接口方法是如何协同工作的:

    • 首先beging()方法告诉Sink参与排序的元素个数,方便确定中间结果容器的的大小;
    • 之后通过accept()方法将元素添加到中间结果当中,最终执行时调用者会不断调用该方法,直到遍历所有元素;
    • 最后end()方法告诉Sink所有元素遍历完毕,启动排序步骤,排序完成后将结果传递给下游的Sink;
    • 如果下游的Sink是短路操作,将结果传递给下游时不断询问下游cancellationRequested()是否可以结束处理。

    3.3 叠加之后的操作如何执行

    Sink完美封装了Stream每一步操作,并给出了[处理->转发]的模式来叠加操作。这一连串的齿轮已经咬合,就差最后一步拨动齿轮启动执行。是什么启动这一连串的操作呢?也许你已经想到了启动的原始动力就是终止操作(Terminal Operation),一旦调用某个终止操作,就会触发整个流水线的执行。

    终止操作之后不能再有别的操作,所以终止操作不会创建新的流水线阶段(Stage),直观的说就是流水线的链表不会在往后延伸了。终止操作会创建一个包装了自己操作的Sink,这也是流水线中最后一个Sink,这个Sink只需要处理数据而不需要将结果传递给下游的Sink(因为没有下游)。对于Sink的[处理->转发]模型,终止操作的Sink就是调用链的出口。


    我们再来考察一下上游的Sink是如何找到下游Sink的。一种可选的方案是在PipelineHelper中设置一个Sink字段,在流水线中找到下游Stage并访问Sink字段即可。但Stream类库的设计者没有这么做,而是设置了一个Sink AbstractPipeline.opWrapSink(int flags, Sink downstream)方法来得到Sink,该方法的作用是返回一个新的包含了当前Stage代表的操作以及能够将结果传递给downstream的Sink对象。为什么要产生一个新对象而不是返回一个Sink字段?这是因为使用opWrapSink()可以将当前操作与下游Sink(上文中的downstream参数)结合成新Sink。试想只要从流水线的最后一个Stage开始,不断调用上一个Stage的opWrapSink()方法直到最开始(不包括stage0,因为stage0代表数据源,不包含操作),就可以得到一个代表了流水线上所有操作的Sink,用代码表示就是这样:

    // AbstractPipeline.wrapSink()
    // 从下游向上游不断包装Sink。如果最初传入的sink代表结束操作,
    // 函数返回时就可以得到一个代表了流水线上所有操作的Sink。
    final <P_IN> Sink<P_IN> wrapSink(Sink<E_OUT> sink) {
        ...
        for (AbstractPipeline p=AbstractPipeline.this; p.depth > 0; p=p.previousStage) {
            sink = p.opWrapSink(p.previousStage.combinedFlags, sink);
        }
        return (Sink<P_IN>) sink;
    }
    

    现在流水线上从开始到结束的所有的操作都被包装到了一个Sink里,执行这个Sink就相当于执行整个流水线,执行Sink的代码如下:

    // AbstractPipeline.copyInto(), 对spliterator代表的数据执行wrappedSink代表的操作。
    final <P_IN> void copyInto(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator) {
        ...
        if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) {
            wrappedSink.begin(spliterator.getExactSizeIfKnown());// 通知开始遍历
            spliterator.forEachRemaining(wrappedSink);// 迭代
            wrappedSink.end();// 通知遍历结束
        }
        ...
    }
    

    上述代码首先调用wrappedSink.begin()方法告诉Sink数据即将到来,然后调用spliterator.forEachRemaining()方法对数据进行迭代(Spliterator是容器的一种迭代器,参阅),最后调用wrappedSink.end()方法通知Sink数据处理结束。逻辑如此清晰。

    3.4 执行后的结果在哪里

    流水线上所有操作都执行后,用户所需要的结果(如果有)在哪里?首先要说明的是不是所有的Stream结束操作都需要返回结果,有些操作只是为了使用其副作用(Side-effects),比如使用Stream.forEach()方法将结果打印出来就是常见的使用副作用的场景(事实上,除了打印之外其他场景都应避免使用副作用),对于真正需要返回结果的结束操作结果存在哪里呢?

    特别说明:副作用不应该被滥用,也许你会觉得在Stream.forEach()里进行元素收集是个不错的选择,就像下面代码中那样,但遗憾的是这样使用的正确性和效率都无法保证,因为Stream可能会并行执行。大多数使用副作用的地方都可以使用归约操作更安全和有效的完成。

    // 错误的收集方式
    ArrayList<String> results = new ArrayList<>();
    stream.filter(s -> pattern.matcher(s).matches())
          .forEach(s -> results.add(s));  // Unnecessary use of side-effects!
    // 正确的收集方式
    List<String>results =
         stream.filter(s -> pattern.matcher(s).matches())
                 .collect(Collectors.toList());  // No side-effects!
    

    需要返回结果的流水线结果存在哪里呢?这要分不同的情况讨论,下表给出了各种有返回结果的Stream结束操作。

    • 1.对于表中返回boolean或者Optional的操作的操作,由于值返回一个值,只需要在对应的Sink中记录这个值,等到执行结束时返回就可以了。
    • 2.对于归约操作,最终结果放在用户调用时指定的容器中(容器类型通过收集器指定)。collect(), reduce(), max(), min()都是归约操作,虽然max()和min()也是返回一个Optional,但事实上底层是通过调用reduce()方法实现的。
    • 3.对于返回是数组的情况,毫无疑问的结果会放在数组当中。这么说当然是对的,但在最终返回数组之前,结果其实是存储在一种叫做Node的数据结构中的。Node是一种多叉树结构,元素存储在树的叶子当中,并且一个叶子节点可以存放多个元素。这样做是为了并行执行方便。

    4.示例

    public class StreamTest {
        public static void main(String[] args) {
            String[] strs = {"ABS", "dedfa", "American", "Becareful"};
            ArrayList<String> strings = new ArrayList<>(Arrays.asList(strs));
            OptionalInt longestStringLengthStartingWithA
                    = strings.stream()
                    .filter(s -> s.startsWith("A"))
                    .mapToInt(String::length)
                    .max();
            System.out.println(longestStringLengthStartingWithA);
        }
    }
    

    4.1 Colletion.stream()

        default Stream<E> stream() {
            return StreamSupport.stream(spliterator(), false);
        }
    

    调用ArrayList.spliterator():

        @Override
        public Spliterator<E> spliterator() {
            return new ArrayListSpliterator<>(this, 0, -1, 0);
        }
    
        public static <T> Stream<T> stream(Spliterator<T> spliterator, boolean parallel) {
            Objects.requireNonNull(spliterator);
            return new ReferencePipeline.Head<>(spliterator,
                                                StreamOpFlag.fromCharacteristics(spliterator),
                                                parallel);
        }
    

    4.2 filter

    使用了匿名类的技巧:

    • 构建StatelessOp
    • 构建Sink.ChainedReference
        @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);
                        }
                    };
                }
            };
        }
    

    4.3 mapToInt

        @Override
        public final IntStream mapToInt(ToIntFunction<? super P_OUT> mapper) {
            Objects.requireNonNull(mapper);
            return new IntPipeline.StatelessOp<P_OUT>(this, StreamShape.REFERENCE,
                                                  StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) {
                @Override
                Sink<P_OUT> opWrapSink(int flags, Sink<Integer> sink) {
                    return new Sink.ChainedReference<P_OUT, Integer>(sink) {
                        @Override
                        public void accept(P_OUT u) {
                            downstream.accept(mapper.applyAsInt(u));
                        }
                    };
                }
            };
        }
    

    4.4 IntPipeline.max

        @Override
        public final OptionalInt max() {
            return reduce(Math::max);
        }
    
        @Override
        public final OptionalInt reduce(IntBinaryOperator op) {
            return evaluate(ReduceOps.makeInt(op));
        }
    

    4.4.1 构造ReduceOp

        public static TerminalOp<Integer, OptionalInt>
        makeInt(IntBinaryOperator operator) {
            Objects.requireNonNull(operator);
            class ReducingSink
                    implements AccumulatingSink<Integer, OptionalInt, ReducingSink>, Sink.OfInt {
                private boolean empty;
                private int state;
    
                public void begin(long size) {
                    empty = true;
                    state = 0;
                }
    
                @Override
                public void accept(int t) {
                    if (empty) {
                        empty = false;
                        state = t;
                    }
                    else {
                        state = operator.applyAsInt(state, t);
                    }
                }
    
                @Override
                public OptionalInt get() {
                    return empty ? OptionalInt.empty() : OptionalInt.of(state);
                }
    
                @Override
                public void combine(ReducingSink other) {
                    if (!other.empty)
                        accept(other.state);
                }
            }
            return new ReduceOp<Integer, OptionalInt, ReducingSink>(StreamShape.INT_VALUE) {
                @Override
                public ReducingSink makeSink() {
                    return new ReducingSink();
                }
            };
        }
    

    4.4.2 evaluate

        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()));
        }
    
       /**
         * Get the source spliterator for this pipeline stage.  For a sequential or
         * stateless parallel pipeline, this is the source spliterator.  For a
         * stateful parallel pipeline, this is a spliterator describing the results
         * of all computations up to and including the most recent stateful
         * operation.
         */
        @SuppressWarnings("unchecked")
        private Spliterator<?> sourceSpliterator(int terminalFlags) {
            // Get the source spliterator of the pipeline
            Spliterator<?> spliterator = null;
            if (sourceStage.sourceSpliterator != null) {
                spliterator = sourceStage.sourceSpliterator;
                sourceStage.sourceSpliterator = null;
            }
            else if (sourceStage.sourceSupplier != null) {
                spliterator = (Spliterator<?>) sourceStage.sourceSupplier.get();
                sourceStage.sourceSupplier = null;
            }
            else {
                throw new IllegalStateException(MSG_CONSUMED);
            }
    
            if (isParallel() && sourceStage.sourceAnyStateful) {
                // Adapt the source spliterator, evaluating each stateful op
                // in the pipeline up to and including this pipeline stage.
                // The depth and flags of each pipeline stage are adjusted accordingly.
                int depth = 1;
                for (@SuppressWarnings("rawtypes") AbstractPipeline u = sourceStage, p = sourceStage.nextStage, e = this;
                     u != e;
                     u = p, p = p.nextStage) {
    
                    int thisOpFlags = p.sourceOrOpFlags;
                    if (p.opIsStateful()) {
                        depth = 0;
    
                        if (StreamOpFlag.SHORT_CIRCUIT.isKnown(thisOpFlags)) {
                            // Clear the short circuit flag for next pipeline stage
                            // This stage encapsulates short-circuiting, the next
                            // stage may not have any short-circuit operations, and
                            // if so spliterator.forEachRemaining should be used
                            // for traversal
                            thisOpFlags = thisOpFlags & ~StreamOpFlag.IS_SHORT_CIRCUIT;
                        }
    
                        spliterator = p.opEvaluateParallelLazy(u, spliterator);
    
                        // Inject or clear SIZED on the source pipeline stage
                        // based on the stage's spliterator
                        thisOpFlags = spliterator.hasCharacteristics(Spliterator.SIZED)
                                ? (thisOpFlags & ~StreamOpFlag.NOT_SIZED) | StreamOpFlag.IS_SIZED
                                : (thisOpFlags & ~StreamOpFlag.IS_SIZED) | StreamOpFlag.NOT_SIZED;
                    }
                    p.depth = depth++;
                    p.combinedFlags = StreamOpFlag.combineOpFlags(thisOpFlags, u.combinedFlags);
                }
            }
    
            if (terminalFlags != 0)  {
                // Apply flags from the terminal operation to last pipeline stage
                combinedFlags = StreamOpFlag.combineOpFlags(terminalFlags, combinedFlags);
            }
    
            return spliterator;
        }
    
    
            @Override
            public <P_IN> R evaluateSequential(PipelineHelper<T> helper,
                                               Spliterator<P_IN> spliterator) {
                return helper.wrapAndCopyInto(makeSink(), spliterator).get();
            }
    
        @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;
        }
    
        @Override
        @SuppressWarnings("unchecked")
        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;
        }
    
        static abstract class ChainedReference<T, E_OUT> implements Sink<T> {
            protected final Sink<? super E_OUT> downstream;
    
            public ChainedReference(Sink<? super E_OUT> downstream) {
                this.downstream = Objects.requireNonNull(downstream);
            }
    

    使用opWrapSink()可以将当前操作与下游Sink(上文中的downstream参数)结合成新Sink。

    这里调用的opWrapSink是上面mapToInt和filter中重写的方法。,在这里调用是为了构造Sink.ChainedReference,通过downstream将其单向链接起来。

        @Override
        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);
            }
        }
    

    其中forEachRemaining的调用:

            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();
            }
    

    4.5 总结

    核心都是在spliterator.forEachRemaining(wrappedSink):

    • 1)迭代器spliterator遍历流的每个元素
    • 2)针对每个元素调用action.accept(e)

    前面的操作都是在组合这个wrappedSink也即action,将所有action从前到后串成一个单向链表。

    该例子实现了一次迭代实现所有操作的功能。

    参考

    相关文章

      网友评论

          本文标题:Stream流水线原理

          本文链接:https://www.haomeiwen.com/subject/wvralctx.html