我了解到,通常Java流不会拆分。但是,我们有一个冗长且费时的管道,在管道的最后,我们有两种不同类型的处理程序共享管道的第一部分。
由于数据的大小,存储中间流产品不是可行的解决方案。都没有两次运行管道。
基本上,我们正在寻找一种解决方案,该解决方案是对流进行操作,从而产生两个(或更多个)流,这些流被延迟填充并可以并行使用。这样,我的意思是,如果流A分为流B和C,则当流B和C消耗10个元素时,流A消耗并提供了这10个元素,但是如果流B然后尝试消耗更多的元素,则它将阻塞直到流C也消耗它们。
是否存在针对此问题的预制解决方案或我们可以查看的任何库?如果不是这样,如果我们要自己实现这一目标,我们将从哪里开始呢?还是有一个完全不实施的迫切理由?
答案 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
还将缓存甚至不再需要的元素,因为它们已经被streamB
和streamC
占用(因此,如果您无法负担将它们保留在内存中,则可以不是您的解决方案); streamB
和streamC
不会互相阻挡。 免责声明:我是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