即时添加元素到Java 8并行流

时间:2016-10-17 11:37:15

标签: java multithreading concurrency parallel-processing java-stream

目标是在Java 8流的帮助下处理连续的元素流。因此,在处理该流时,会将元素添加到并行流的数据源中。

Javadoc of Streams描述了“无干扰”部分中的以下属性:

  

对于大多数数据源,防止干扰意味着确保在流管道执行期间根本不修改数据源。值得注意的例外是其源是并发集合的流,这些集合专门用于处理并发修改。并发流源是Spliterator报告CONCURRENT特征的源。

这就是我们尝试使用ConcurrentLinkedQueue的原因,它为

返回true
new ConcurrentLinkedQueue<Integer>().spliterator().hasCharacteristics(Spliterator.CONCURRENT)

没有明确说明,在并行流中使用时不得修改数据源。

在我们对流中每个元素的示例中,递增的计数器值被添加到队列中,该队列是流的数据源,直到计数器大于N. 通过调用queue.stream(),顺序执行时一切正常:

import static org.junit.Assert.assertEquals;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;

public class StreamTest {
    public static void main(String[] args) {
        final int N = 10000;
        assertEquals(N, testSequential(N));
    }

    public static int testSequential(int N) {
        final AtomicInteger counter = new AtomicInteger(0);
        final AtomicInteger check = new AtomicInteger(0);
        final Queue<Integer> queue = new ConcurrentLinkedQueue<Integer>();

        for (int i = 0; i < N / 10; ++i) {
            queue.add(counter.incrementAndGet());
        }

        Stream<Integer> stream = queue.stream();
        stream.forEach(i -> {
            System.out.println(i);

            int j = counter.incrementAndGet();

            check.incrementAndGet();
            if (j <= N) {
                queue.add(j);
            }
        });
        stream.close();
        return check.get();
    }
}

作为第二次尝试,流是并行的并抛出java.lang.AssertionError,因为check小于N并且并未处理队列中的每个元素。流可能已提前完成执行,因为队列可能在某个时间点变空。

import static org.junit.Assert.assertEquals;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;

public class StreamTest {
    public static void main(String[] args) {
        final int N = 10000;
        assertEquals(N, testParallel1(N));
    }

    public static int testParallel1(int N) {
        final AtomicInteger counter = new AtomicInteger(0);
        final AtomicInteger check = new AtomicInteger(0);
        final Queue<Integer> queue = new ConcurrentLinkedQueue<Integer>();

        for (int i = 0; i < N / 10; ++i) {
            queue.add(counter.incrementAndGet());
        }

        Stream<Integer> stream = queue.parallelStream();
        stream.forEach(i -> {
            System.out.println(i);

            int j = counter.incrementAndGet();

            check.incrementAndGet();
            if (j <= N) {
                queue.add(j);
            }
        });
        stream.close();
        return check.get();
    }
}

下一次尝试是在连续流“真正”结束(队列为空)后发出主线程信号,然后关闭流对象。这里的问题是流对象似乎只从队列中读取元素一次或者至少不连续读取,并且永远不会到达流的“真实”端。

import static org.junit.Assert.assertEquals;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Stream;

public class StreamTest {

    public static void main(String[] args) {
        final int N = 10000;
        try {
            assertEquals(N, testParallel2(N));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static int testParallel2(int N) throws InterruptedException {
        final Lock lock = new ReentrantLock();
        final Condition cond = lock.newCondition();

        final AtomicInteger counter = new AtomicInteger(0);
        final AtomicInteger check = new AtomicInteger(0);
        final Queue<Integer> queue = new ConcurrentLinkedQueue<Integer>();

        for (int i = 0; i < N / 10; ++i) {
            queue.add(counter.incrementAndGet());
        }

        Stream<Integer> stream = queue.parallelStream();
        stream.forEach(i -> {
            System.out.println(i);

            int j = counter.incrementAndGet();

            lock.lock();
            check.incrementAndGet();
            if (j <= N) {
                queue.add(j);
            } else {
                cond.signal();
            }
            lock.unlock();
        });

        lock.lock();
        while (check.get() < N) {
            cond.await();
        }
        lock.unlock();
        stream.close();
        return check.get();
    }
}

由此产生的问题是:

  • 我们做错了吗?
  • Stream API是否未指明甚至错误使用?
  • 我们怎样才能达到理想的行为呢?

2 个答案:

答案 0 :(得分:2)

“修改Stream的来源不会破坏它”与您的假设“修改将通过正在进行的Stream操作反映”之间存在显着差异。

CONCURRENT属性意味着源的修改是允许的,即它永远不会抛出ConcurrentModificationException,但这并不意味着你可以依赖关于是否反映这些变化的具体行为。

documentation of the CONCURRENT flag本身说:

  

大多数并发收集保持一致性策略,保证Spliterator构造点存在的元素的准确性,但可能不反映后续的添加或删除。

此Stream行为与已知的ConcurrentLinkedQueue行为一致:

  

迭代器弱一致,在迭代器创建时或之后的某个时刻返回反映队列状态的元素。他们抛出ConcurrentModificationException,并可能与其他操作同时进行。自创建迭代器以来队列中包含的元素将只返回一次。

很难说如何“以其他方式实现所需的行为”,就像你没有以代码之外的任何形式描述“期望的行为”一样,可以简单地用

代替
public static int testSequential(int N) {
    return N;
}
public static int testParallel1(int N) {
    return N;
}

因为那是唯一可观察到的效果......考虑redefining your problem ......

答案 1 :(得分:0)

可以连续生成流,也可以从已修改的集合生成流,也不设计为连续运行。它旨在处理流启动时可用的元素,并在处理完这些元素后返回。一旦到达终点,它就会停止。

  

我们怎样才能达到理想的行为?

您需要使用不同的方法。我会使用ExecutorService来传递您想要执行的提交任务。

另一种方法是使用连续流,当没有可用结果时阻塞。注意:这将锁定并行流使用的Common ForkJoinPool,其他代码无法使用它。