Java Stream API:为什么区分顺序和并行执行模式?

时间:2014-04-09 00:23:18

标签: java parallel-processing java-8 java-stream

来自Stream javadoc

  

流管道可以顺序执行,也可以并行执行。此执行模式是流的属性。通过初始选择顺序或并行执行来创建流。

我的假设:

  1. 顺序/并行流之间没有功能差异。输出永远不会受执行模式的影响。
  2. 由于性能提升,给定适当数量的内核和问题大小来证明开销是一个优选的并行流。
  3. 我们想编写一次代码并在任何地方运行而不必关心硬件(毕竟这是Java)。
  4. 假设这些假设是有效的(一些元假设没有错),在api中暴露执行模式的价值是什么?

    看起来您应该能够声明Stream,顺序/并行执行的选择应该在下面的层中自动处理,可以通过库代码或JVM本身作为运行时可用的核心,问题的大小等等。

    当然,假设并行流也可以在单个核心机器上运行,也许只是总是使用并行流来实现这一点。但这真的很难看 - 为什么我的代码中的并行流显式引用它是默认选项?

    即使存在您故意想要使用顺序流进行硬编码的情况 - 为什么不仅仅为此目的使用子接口SequentialStream,而不是污染{{ 1}}执行模式切换?

3 个答案:

答案 0 :(得分:26)

  

看起来你应该能够声明一个Stream,并且顺序/并行执行的选择应该在下面的层中自动处理,可以通过库代码或JVM本身作为运行时可用核心的函数来处理,问题的大小等。

现实情况是,a)流是一个库,并没有特殊的JVM魔法,并且b)你无法真正设计一个足够聪明的库来自动找出在这种特殊情况下正确决定的内容。没有合理的方法来估计特定功能在没有运行的情况下会花费多少 - 即使你可以反省它的实现,你也可以 - 现在你将基准引入每个流操作,试图弄清楚是否并行化将是值得并行开销的成本。这是不切实际的,特别是考虑到你事先并不知道并行性开销有多糟糕。

  

由于性能提升,给定适当数量的内核和问题大小以证明开销是一个优选的并行流。

在实践中并非总是如此。有些任务非常小,以至于它们不值得并行化,而且并行性总是会产生一些开销。 (坦率地说,大多数程序员倾向于过高估计并行性的有用性,当它真正伤害性能时,它会随处可见。)

基本上,这是一个很难解决的问题,你基本上不得不把它推到程序员身上。

答案 1 :(得分:3)

  

顺序/并行之间没有功能差异   流。输出永远不会受执行模式的影响。

顺序/并行流执行之间存在差异。 在下面的代码TEST_2中,结果显示并行线程执行比顺序方式快得多。

  

并行流   在适当数量的核心和问题的情况下,总是更可取的   由于性能提升,大小可以证明开销是合理的。

不是真的。如果任务不值得(简单任务)在并行线程中执行,那么我们只是在为代码添加开销。 TEST_1结果显示了这一点。另请注意,如果所有工作线程都忙于一个并行执行任务;然后代码中其他地方的其他并行流操作将等待它。

  

我们想要   编写一次代码并在任何地方运行而无需关心   硬件(毕竟这是Java)。

因为只有程序员知道;是否值得并行/顺序执行此任务,而不管CPU是什么。因此,java API向开发人员公开了这两个选项。

import java.util.ArrayList;
import java.util.List;

/*
 * Performance test over internal(parallel/sequential) and external iterations.
 * https://docs.oracle.com/javase/tutorial/collections/streams/parallelism.html
 * 
 * 
 * Parallel computing involves dividing a problem into subproblems, 
 * solving those problems simultaneously (in parallel, with each subproblem running in a separate thread),
 *  and then combining the results of the solutions to the subproblems. Java SE provides the fork/join framework, 
 *  which enables you to more easily implement parallel computing in your applications. However, with this framework, 
 *  you must specify how the problems are subdivided (partitioned). 
 *  With aggregate operations, the Java runtime performs this partitioning and combining of solutions for you.
 * 
 * Limit the parallelism that the ForkJoinPool offers you. You can do it yourself by supplying the -Djava.util.concurrent.ForkJoinPool.common.parallelism=1,
 *  so that the pool size is limited to one and no gain from parallelization
 *  
 *  @see ForkJoinPool
 *  https://docs.oracle.com/javase/tutorial/essential/concurrency/forkjoin.html
 *  
 *  ForkJoinPool, that pool creates a fixed number of threads (default: number of cores) and 
 *  will never create more threads (unless the application indicates a need for those by using managedBlock).
 *   *  http://stackoverflow.com/questions/10797568/what-determines-the-number-of-threads-a-java-forkjoinpool-creates
 *  
 */
public class IterationThroughStream {
    private static boolean found = false;
    private static List<Integer> smallListOfNumbers = null;
    public static void main(String[] args) throws InterruptedException {


        // TEST_1
        List<String> bigListOfStrings = new ArrayList<String>();
        for(Long i = 1l; i <= 1000000l; i++) {
            bigListOfStrings.add("Counter no: "+ i);
        }

        System.out.println("Test Start");
        System.out.println("-----------");
        long startExternalIteration = System.currentTimeMillis();
        externalIteration(bigListOfStrings);
        long endExternalIteration = System.currentTimeMillis();
        System.out.println("Time taken for externalIteration(bigListOfStrings) is :" + (endExternalIteration - startExternalIteration) + " , and the result found: "+ found);

        long startInternalIteration = System.currentTimeMillis();
        internalIteration(bigListOfStrings);
        long endInternalIteration = System.currentTimeMillis();
        System.out.println("Time taken for internalIteration(bigListOfStrings) is :" + (endInternalIteration - startInternalIteration) + " , and the result found: "+ found);





        // TEST_2
        smallListOfNumbers = new ArrayList<Integer>();
        for(int i = 1; i <= 10; i++) {
            smallListOfNumbers.add(i);
        }

        long startExternalIteration1 = System.currentTimeMillis();
        externalIterationOnSleep(smallListOfNumbers);
        long endExternalIteration1 = System.currentTimeMillis();
        System.out.println("Time taken for externalIterationOnSleep(smallListOfNumbers) is :" + (endExternalIteration1 - startExternalIteration1));

        long startInternalIteration1 = System.currentTimeMillis();
        internalIterationOnSleep(smallListOfNumbers);
        long endInternalIteration1 = System.currentTimeMillis();
        System.out.println("Time taken for internalIterationOnSleep(smallListOfNumbers) is :" + (endInternalIteration1 - startInternalIteration1));




        // TEST_3
        Thread t1 = new Thread(IterationThroughStream :: internalIterationOnThread);
        Thread t2 = new Thread(IterationThroughStream :: internalIterationOnThread);
        Thread t3 = new Thread(IterationThroughStream :: internalIterationOnThread);
        Thread t4 = new Thread(IterationThroughStream :: internalIterationOnThread);

        t1.start();
        t2.start();
        t3.start();
        t4.start();

        Thread.sleep(30000);
    }


    private static boolean externalIteration(List<String> bigListOfStrings) {
        found = false;
        for(String s : bigListOfStrings) {
            if(s.equals("Counter no: 1000000")) {
                found = true;
            }
        }
        return found;
    }

    private static boolean internalIteration(List<String> bigListOfStrings) {
        found = false;
        bigListOfStrings.parallelStream().forEach(
                (String s) -> { 
                    if(s.equals("Counter no: 1000000")){  //Have a breakpoint to look how many threads are spawned.
                        found = true;
                    }

                }
            );
        return found;       
    }


    private static boolean externalIterationOnSleep(List<Integer> smallListOfNumbers) {
        found = false;
        for(Integer s : smallListOfNumbers) {
            try {
                Thread.sleep(100);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return found;
    }

    private static boolean internalIterationOnSleep(List<Integer> smallListOfNumbers) {
        found = false;
        smallListOfNumbers.parallelStream().forEach( //Removing parallelStream() will behave as single threaded (sequential access).
                (Integer s) -> {
                    try {
                        Thread.sleep(100); //Have a breakpoint to look how many threads are spawned.
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            );
        return found;       
    }

    public static void internalIterationOnThread() {
        smallListOfNumbers.parallelStream().forEach(
                (Integer s) -> {
                    try {
                        /*
                         * DANGEROUS
                         * This will tell you that if all the 7 FJP(Fork join pool) worker threads are blocked for one single thread (e.g. t1), 
                         * then other normal three(t2 - t4) thread wont execute, will wait for FJP worker threads. 
                         */
                        Thread.sleep(100); //Have a breakpoint here.
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            );
    }
}

答案 2 :(得分:0)

  

似乎您应该只能够声明一个Stream,并且顺序/并行执行的选择应该在下面的一层中自动进行处理,可以通过库代码或JVM本身作为运行时可用核心的功能,问题的大小等。

要添加到已经给出的答案中:

那是一个相当大胆的假设。想象一下模拟一个用来训练某种形式的AI的棋盘游戏,很容易并行执行不同的游戏流程-只需创建一个新实例并使其在自己的线程上运行即可。由于它不与其他游戏共享任何状态,因此您甚至不必在游戏逻辑中考虑多线程问题。另一方面,如果并行化游戏逻辑本身,则会遇到各种各样的多线程问题,并且很可能为复杂性甚至性能付出高昂的代价。

控制流的行为将为您(适当地受限制)灵活性,这本身就是良好库设计的关键功能。