并行流在不同的操作下能否正常运行?

时间:2018-12-06 05:14:42

标签: java java-8 java-stream

我正在阅读有关无国籍的信息,并在doc中发现了这一点:

  

如果以下原因导致流管道结果可能不确定或不正确   流操作的行为参数是有状态的。一种   有状态的lambda(或其他实现了适当的   功能接口)是一个其结果取决于任何状态的接口   在执行流水线期间可能会发生变化。

现在,如果我有一个字符串列表(例如,strList),然后尝试使用并行流通过以下方式从其中删除重复的字符串:

List<String> resultOne = strList.parallelStream().distinct().collect(Collectors.toList());

或者如果我们希望不区分大小写:

List<String> result2 = strList.parallelStream().map(String::toLowerCase)
                       .distinct().collect(Collectors.toList());

此代码是否有问题,因为并行流会拆分输入,并且在一个块中截然不同并不一定意味着在整个输入中截然不同?

编辑(以下答案的快速摘要)

distinct是一个有状态操作,在有状态中间操作的情况下,并行流可能需要多次通过或大量缓冲开销。如果元素的顺序无关紧要,distinct也可以更有效地实现。 同样按照doc

  

对于有序流,不同元素的选择是稳定的(对于   重复的元素,该元素在遭遇中首先出现   保留顺序。)对于无序流,没有稳定性保证   制成。

但是在有序流并行运行的情况下,distinct可能不稳定-意味着在重复的情况下它将保留任意元素,而不必像distinct那样保留第一个元素。

来自link

  

在内部,distinct()操作保留一个Set,该Set包含   以前已经看到过的元素,但是它被埋在   操作,我们无法从应用程序代码中获取它。

因此,在并行流的情况下,可能会消耗整个流或可能使用CHM(例如ConcurrentHashMap.newKeySet())。对于有序的对象,最有可能会使用LinkedHashSet或类似的结构。

4 个答案:

答案 0 :(得分:11)

粗略指出doc重点增强)的相关部分:

  

中间操作又分为无状态操作和无状态操作   状态操作。无状态操作,例如过滤器和地图,   处理新的元素时不保留先前看到的元素的状态   元素-每个元素都可以独立于操作进行处理   在其他元素上。 状态操作,例如不同的和已排序的,   在处理时可能会合并先前看到的元素的状态   新元素

     

状态操作可能需要先处理所有输入   产生结果。例如,一个人不能从   对流进行排序,直到看到流的所有元素为止。 作为   结果,在并行计算下,一些包含有状态的管道   中间操作可能需要多次通过数据,或者可能   需要缓冲大量数据。仅包含管道   无状态中间操作可以一次处理,   无论是顺序的还是并行的,具有最少的数据缓冲

如果您进一步阅读(关于订购的部分):

  

流可能有也可能没有定义的遇到顺序。是否   流的遭遇顺序取决于来源和   中间作业。 某些流源(例如列表或   数组)在本质上是有序的,而其他数组(例如HashSet)   不是。一些中间操作(例如sorted())可能会施加   在原本无序的流上遇到订单,其他人可能会   使无序流呈现有序,例如BaseStream.unordered()。   此外,某些终端操作可能会忽略遇到顺序,例如   forEach()。

...

  

对于并行流,有时可以放宽排序约束   使执行效率更高。 某些汇总操作,例如   过滤重复项(distinct())或分组归约   如果(Collectors.groupingBy())可以更有效地实现   元素的顺序不相关。同样,   本质上与遇到顺序有关,例如limit()可能需要   缓冲以确保适当的订购,从而损害了   并行性。 如果流具有遇到顺序,但   用户并不特别在乎那个碰头顺序   使用unordered()对流进行排序可以改善并行   有状态或终端操作的性能。但是,大多数   流管道,例如上面的“块权重之和”示例,   即使在排序约束下仍然可以有效地并行化。

最后,

  • distinct可以很好地处理并行流,但是您可能已经知道,它必须先消耗掉整个流,然后才能继续使用,这可能会占用大量内存。
  • 如果项目的来源是无序集合(例如哈希集)或流是unordered(),则distinct不必担心对输出进行排序,因此效率很高

解决方案是,如果您不担心定单并希望获得更高的性能,则将.unordered()添加到流管道中。

List<String> result2 = strList.parallelStream()
                              .unordered()
                              .map(String::toLowerCase)
                              .distinct()
                              .collect(Collectors.toList());

A,Java中没有(可用的内置)并发哈希集(除非他们对ConcurrentHashMap很聪明),所以我只能为您留下一个遗憾,即使用常规Java以阻塞的方式实现了distinct组。在这种情况下,我看不到做并行的区别有什么好处。


编辑:我讲得太早了。使用具有独特性的并行流可能会有一些好处。看来distinct的实现比我最初想象的要聪明得多。参见@Eugene's answer

答案 1 :(得分:3)

这不会有问题(问题有可能是错误的结果),但正如API注释所说

  

在并行管道中为distinct()保持稳定性是相对昂贵的

但是如果要关注性能并且如果stability不是问题(即结果相对于所处理的集合而言元素的顺序不同),则请遵循API的说明

  

使用BaseStream.unordered()删除排序约束   导致在   并行管道,

我想为什么不对distinct的并行流和顺序流的性能进行基准测试

public static void main(String[] args) {
        List<String> strList = Arrays.asList("cat", "nat", "hat", "tat", "heart", "fat", "bat", "lad", "crab", "snob");

        List<String> words = new Vector<>();


        int wordCount = 1_000_000; // no. of words in the list words
        int avgIter = 10; // iterations to run to find average running time

        //populate a list randomly with the strings in `strList`
        for (int i = 0; i < wordCount; i++) 
            words.add(strList.get((int) Math.round(Math.random() * (strList.size() - 1))));





        //find out average running times
        long starttime, pod = 0, pud = 0, sod = 0;
        for (int i = 0; i < avgIter; i++) {
            starttime = System.currentTimeMillis();
            List<String> parallelOrderedDistinct = words.parallelStream().distinct().collect(Collectors.toList());
            pod += System.currentTimeMillis() - starttime;

            starttime = System.currentTimeMillis();
            List<String> parallelUnorderedDistinct =
                    words.parallelStream().unordered().distinct().collect(Collectors.toList());
            pud += System.currentTimeMillis() - starttime;

            starttime = System.currentTimeMillis();
            List<String> sequentialOrderedDistinct = words.stream().distinct().collect(Collectors.toList());
            sod += System.currentTimeMillis() - starttime;
        }

        System.out.println("Parallel ordered time in ms: " + pod / avgIter);
        System.out.println("Parallel unordered time in ms: " + pud / avgIter);
        System.out.println("Sequential implicitly ordered time in ms: " + sod / avgIter);
    }

上面的内容是由open-jdk 8编译的,并在i3第六代(4个逻辑内核)上的openjdk的jre 8(没有特定于jvm的参数)上运行,我得到了这些结果

看起来像是经过一定的否定。在元素数量中,有序并行速度更快,而具有讽刺意味的是,无序并行速度最慢。其背后的原因(感谢@Hulk)是由于其实现方式(使用HashSet)。因此,通常的规则是,如果您有几个元素并且大量重复了几个数量级,您可能会受益于{ {1}}。

1)

parallel()

2)

Parallel ordered time in ms: 52
Parallel unordered time in ms: 81
Sequential implicitly ordered time in ms: 35

3)

Parallel ordered time in ms: 48
Parallel unordered time in ms: 83
Sequential implicitly ordered time in ms: 34

无序并行比两者慢两倍。

然后我将Parallel ordered time in ms: 36 Parallel unordered time in ms: 70 Sequential implicitly ordered time in ms: 32 提升到wordCount,这些就是结果

1)

5_000_000

2)

Parallel ordered time in ms: 93
Parallel unordered time in ms: 363
Sequential implicitly ordered time in ms: 123

3)

Parallel ordered time in ms: 100
Parallel unordered time in ms: 363
Sequential implicitly ordered time in ms: 124

然后转到Parallel ordered time in ms: 89 Parallel unordered time in ms: 365 Sequential implicitly ordered time in ms: 118

1)

10_000_000

2)

Parallel ordered time in ms: 148
Parallel unordered time in ms: 725
Sequential implicitly ordered time in ms: 218

3)

Parallel ordered time in ms: 150
Parallel unordered time in ms: 749
Sequential implicitly ordered time in ms: 224

答案 2 :(得分:3)

您似乎错过了提供的文档和实际示例中的很多内容。

  

如果流操作的行为参数是有状态的,则流管道的结果可能不确定或不正确。

在您的示例中,您没有定义任何有状态的操作。文档中的有状态是指您定义的内容,而不是jdk本身实现的内容-例如您的示例中的distinct。但是无论哪种方式,您都可以定义正确的有状态操作,甚至Stuart Marks - working at Oracle/Java, provides such an example

因此,无论您是否并行,在所提供的示例中您都还可以。

distinct(并行)的昂贵部分来自于以下事实:内部必须有一个线程安全的数据结构,该结构应保留不同的元素;在jdk中,如果顺序无关紧要,则使用ConcurrentHashMap;在顺序重要时,则使用LinkedHashSet进行归约。

distinct btw是一个非常聪明的实现,它查看流的源是否已经不同(在这种情况下,它是无操作的),或者查看数据是否已排序,在这种情况下它将对源进行更智能的遍历(因为它知道如果您已经看到一个元素,那么接下来的元素要么是您刚刚看到的元素,要么是另一个元素),或者在内部使用ConcurrentHashMap

答案 3 :(得分:1)

在javadocs中,parallelStream()

  

返回一个可能并行Stream,并将此集合作为源。   该方法允许返回顺序流。

性能:

  1. 让我们考虑一下,我们有多个流(幸运地)被分配给不同的CPU内核。 ArrayList<T>具有基于数组的内部数据表示。或者LinkedList<T>,它需要更多的计算才能并行处理拆分。在这种情况下,ArrayList<T>更好!
  2. stream.unordered().parallel().distinct()的性能优于stream.parallel().distinct()
  

在并行管道中保持distinct()的稳定性是   相对昂贵(要求该操作作为完整   障碍,并有大量的缓冲开销。

因此,就您而言,应该没问题(除非您的List<T>不在意订单)。请阅读下面的说明,

假设您在ArrayList中有4个元素, {“ a”,“ b”,“ a”,“ b”}

现在,如果您不要在调用parallelStream()之前使用distinct(),则仅保留位置0和1处的字符串。(保留顺序,顺序流)< / p>

否则,(如果您使用parallelStream().distinct()),则可以将元素1和2 保留为不重复(它是不稳定的,但结果是相同的{“ a,” b“ },甚至可以是{“ b”,“ a”})。

不稳定的独特操作会随机消除重复项。

最后,

  

在并行计算下,某些管道包含有状态   中间操作可能需要多次通过数据,或者可能   需要缓冲重要数据