将Java流分成两个惰性流,而无需终端操作

时间:2018-07-26 18:24:42

标签: java java-stream

我了解到,通常Java流不会拆分。但是,我们有一个冗长且费时的管道,在管道的最后,我们有两种不同类型的处理程序共享管道的第一部分。

由于数据的大小,存储中间流产品不是可行的解决方案。都没有两次运行管道。

基本上,我们正在寻找一种解决方案,该解决方案是对流进行操作,从而产生两个(或更多个)流,这些流被延迟填充并可以并行使用。这样,我的意思是,如果流A分为流B和C,则当流B和C消耗10个元素时,流A消耗并提供了这10个元素,但是如果流B然后尝试消耗更多的元素,则它将阻塞直到流C也消耗它们。

是否存在针对此问题的预制解决方案或我们可以查看的任何库?如果不是这样,如果我们要自己实现这一目标,我们将从哪里开始呢?还是有一个完全不实施的迫切理由?

2 个答案:

答案 0 :(得分:4)

我不知道可以满足您的阻止要求的功能,但是您可能会对jOOλSeq.duplicate()方法感兴趣:

Stream<T> streamA = Stream.of(/* your data here */);
Tuple2<Seq<T>, Seq<T>> streamTuple = Seq.seq(streamA).duplicate();
Stream<T> streamB = streamTuple.v1();
Stream<T> streamC = streamTuple.v2();

由于此方法在内部使用了SeqBuffer类,因此Stream可以绝对独立地使用(包括并行使用)。

请注意:

  • SeqBuffer还将缓存甚至不再需要的元素,因为它们已经被streamBstreamC占用(因此,如果您无法负担将它们保留在内存中,则可以不是您的解决方案);
  • 就像我在开始时提到的,streamBstreamC不会互相阻挡。

免责声明:我是SeqBuffer类的作者。

答案 1 :(得分:3)

您可以实现自定义Spliterator,以实现这种行为。我们会将您的信息流划分为共同的“来源”和不同的“消费者”。然后,自定义拆分器将元素从源转发到每个使用者。为此,我们将使用BlockingQueue(请参阅this question)。

请注意,这里困难的部分不是分隔符/流,而是队列周围使用者的同步,正如您对问题的评论已表明的那样。不过,尽管您实施了同步,但Spliterator仍可以帮助您使用流。

@SafeVarargs
public static <T> long streamForked(Stream<T> source, Consumer<Stream<T>>... consumers)
{
    return StreamSupport.stream(new ForkingSpliterator<>(source, consumers), false).count();
}

private static class ForkingSpliterator<T>
    extends AbstractSpliterator<T>
{
    private Spliterator<T>   sourceSpliterator;

    private BlockingQueue<T> queue      = new LinkedBlockingQueue<>();

    private AtomicInteger    nextToTake = new AtomicInteger(0);
    private AtomicInteger    processed  = new AtomicInteger(0);

    private boolean          sourceDone;
    private int              consumerCount;

    @SafeVarargs
    private ForkingSpliterator(Stream<T> source, Consumer<Stream<T>>... consumers)
    {
        super(Long.MAX_VALUE, 0);

        sourceSpliterator = source.spliterator();
        consumerCount = consumers.length;

        for (int i = 0; i < consumers.length; i++)
        {
            int index = i;
            Consumer<Stream<T>> consumer = consumers[i];
            new Thread(new Runnable()
            {
                @Override
                public void run()
                {
                    consumer.accept(StreamSupport.stream(new ForkedConsumer(index), false));
                }
            }).start();
        }
    }

    @Override
    public boolean tryAdvance(Consumer<? super T> action)
    {
        sourceDone = !sourceSpliterator.tryAdvance(queue::offer);
        return !sourceDone;
    }

    private class ForkedConsumer
        extends AbstractSpliterator<T>
    {
        private int index;

        private ForkedConsumer(int index)
        {
            super(Long.MAX_VALUE, 0);

            this.index = index;
        }

        @Override
        public boolean tryAdvance(Consumer<? super T> action)
        {
            // take next element when it's our turn
            while (!nextToTake.compareAndSet(index, index + 1))
            {
            }
            T element;
            while ((element = queue.peek()) == null)
            {
                if (sourceDone)
                {
                    // element is null, and there won't be no more, so "terminate" this sub stream
                    return false;
                }
            }

            // push to consumer pipeline
            action.accept(element);

            if (consumerCount == processed.incrementAndGet())
            {
                // start next round
                queue.poll();
                processed.set(0);
                nextToTake.set(0);
            }

            return true;
        }
    }
}

使用这种方法,使用者可以并行处理每个元素,但是在开始下一个元素之前要互相等待。

已知问题 如果其中一个使用者比其他使用者“短”(例如,因为它调用limit()),它还将停止其他使用者,并使线程挂起。


示例

public static void sleep(long millis)
{
    try { Thread.sleep((long) (Math.random() * 30 + millis)); } catch (InterruptedException e) { }
}

streamForked(Stream.of("1", "2", "3", "4", "5"),
             source -> source.map(word -> { sleep(50); return "fast   " + word; }).forEach(System.out::println),
             source -> source.map(word -> { sleep(300); return "slow      " + word; }).forEach(System.out::println),
             source -> source.map(word -> { sleep(50); return "2fast        " + word; }).forEach(System.out::println));

fast   1
2fast        1
slow      1
fast   2
2fast        2
slow      2
2fast        3
fast   3
slow      3
fast   4
2fast        4
slow      4
2fast        5
fast   5
slow      5