Java 8 Stream並行流

qianmoQ發表於2019-01-19

流可以並行執行,以增加大量輸入元素的執行時效能。並行流ForkJoinPool通過靜態ForkJoinPool.commonPool()方法使用公共可用的流。底層執行緒池的大小最多使用五個執行緒 – 具體取決於可用物理CPU核心的數量:

ForkJoinPool commonPool = ForkJoinPool.commonPool();
System.out.println(commonPool.getParallelism()); // 3

在我的機器上,公共池初始化為預設值為3的並行度。通過設定以下JVM引數可以減小或增加此值:

-Djava.util.concurrent.ForkJoinPool.common.parallelism=5

集合支援建立並行元素流的方法parallelStream()。或者,您可以在給定流上呼叫中間方法parallel(),以將順序流轉換為並行流。

為了評估並行流的並行執行行為,下一個示例將有關當前執行緒的資訊列印出來:

Arrays.asList("a1", "a2", "b1", "c2", "c1")
    .parallelStream()
    .filter(s -> {
        System.out.format("filter: %s [%s]
",
            s, Thread.currentThread().getName());
        return true;
    })
    .map(s -> {
        System.out.format("map: %s [%s]
",
            s, Thread.currentThread().getName());
        return s.toUpperCase();
    })
    .forEach(s -> System.out.format("forEach: %s [%s]
",
        s, Thread.currentThread().getName()));

通過調查除錯輸出,我們應該更好地理解哪些執行緒實際用於執行流操作:

filter:  b1 [main]
filter:  a2 [ForkJoinPool.commonPool-worker-1]
map:     a2 [ForkJoinPool.commonPool-worker-1]
filter:  c2 [ForkJoinPool.commonPool-worker-3]
map:     c2 [ForkJoinPool.commonPool-worker-3]
filter:  c1 [ForkJoinPool.commonPool-worker-2]
map:     c1 [ForkJoinPool.commonPool-worker-2]
forEach: C2 [ForkJoinPool.commonPool-worker-3]
forEach: A2 [ForkJoinPool.commonPool-worker-1]
map:     b1 [main]
forEach: B1 [main]
filter:  a1 [ForkJoinPool.commonPool-worker-3]
map:     a1 [ForkJoinPool.commonPool-worker-3]
forEach: A1 [ForkJoinPool.commonPool-worker-3]
forEach: C1 [ForkJoinPool.commonPool-worker-2]

如您所見,並行流利用公共中的所有可用執行緒ForkJoinPool來執行流操作。輸出在連續執行中可能不同,因為實際使用的特定執行緒的行為是非確定性的。

讓我們通過一個額外的流操作來擴充套件該示例:

Arrays.asList("a1", "a2", "b1", "c2", "c1")
    .parallelStream()
    .filter(s -> {
        System.out.format("filter: %s [%s]
",
            s, Thread.currentThread().getName());
        return true;
    })
    .map(s -> {
        System.out.format("map: %s [%s]
",
            s, Thread.currentThread().getName());
        return s.toUpperCase();
    })
    .sorted((s1, s2) -> {
        System.out.format("sort: %s <> %s [%s]
",
            s1, s2, Thread.currentThread().getName());
        return s1.compareTo(s2);
    })
    .forEach(s -> System.out.format("forEach: %s [%s]
",
        s, Thread.currentThread().getName()));

結果可能最初看起來很奇怪:

filter:  c2 [ForkJoinPool.commonPool-worker-3]
filter:  c1 [ForkJoinPool.commonPool-worker-2]
map:     c1 [ForkJoinPool.commonPool-worker-2]
filter:  a2 [ForkJoinPool.commonPool-worker-1]
map:     a2 [ForkJoinPool.commonPool-worker-1]
filter:  b1 [main]
map:     b1 [main]
filter:  a1 [ForkJoinPool.commonPool-worker-2]
map:     a1 [ForkJoinPool.commonPool-worker-2]
map:     c2 [ForkJoinPool.commonPool-worker-3]
sort:    A2 <> A1 [main]
sort:    B1 <> A2 [main]
sort:    C2 <> B1 [main]
sort:    C1 <> C2 [main]
sort:    C1 <> B1 [main]
sort:    C1 <> C2 [main]
forEach: A1 [ForkJoinPool.commonPool-worker-1]
forEach: C2 [ForkJoinPool.commonPool-worker-3]
forEach: B1 [main]
forEach: A2 [ForkJoinPool.commonPool-worker-2]
forEach: C1 [ForkJoinPool.commonPool-worker-1]

似乎sort只在主執行緒上順序執行。實際上,sort在並行流上使用新的Java 8方法Arrays.parallelSort()。如Javadoc中所述,如果排序將按順序或並行執行,則此方法決定陣列的長度:

如果指定陣列的長度小於最小粒度,則使用適當的Arrays.sort方法對其進行排序。

回到reduce一節的例子。我們已經發現組合器函式只是並行呼叫,而不是順序流呼叫。讓我們看看實際涉及哪些執行緒:

List<Person> persons = Arrays.asList(
    new Person("Max", 18),
    new Person("Peter", 23),
    new Person("Pamela", 23),
    new Person("David", 12));

persons
    .parallelStream()
    .reduce(0,
        (sum, p) -> {
            System.out.format("accumulator: sum=%s; person=%s [%s]
",
                sum, p, Thread.currentThread().getName());
            return sum += p.age;
        },
        (sum1, sum2) -> {
            System.out.format("combiner: sum1=%s; sum2=%s [%s]
",
                sum1, sum2, Thread.currentThread().getName());
            return sum1 + sum2;
        });

控制檯輸出顯示累加器和組合器函式在所有可用執行緒上並行執行:

accumulator: sum=0; person=Pamela; [main]
accumulator: sum=0; person=Max;    [ForkJoinPool.commonPool-worker-3]
accumulator: sum=0; person=David;  [ForkJoinPool.commonPool-worker-2]
accumulator: sum=0; person=Peter;  [ForkJoinPool.commonPool-worker-1]
combiner:    sum1=18; sum2=23;     [ForkJoinPool.commonPool-worker-1]
combiner:    sum1=23; sum2=12;     [ForkJoinPool.commonPool-worker-2]
combiner:    sum1=41; sum2=35;     [ForkJoinPool.commonPool-worker-2]

總之,並行流可以為具有大量輸入元素的流帶來良好的效能提升。但請記住,某些並行流操作reduce,collect需要額外的計算(組合操作),這在順序執行時是不需要的。

此外,我們瞭解到所有並行流操作共享相同的JVM範圍ForkJoinPool。因此,您可能希望避免實施慢速阻塞流操作,因為這可能會減慢嚴重依賴並行流的應用程式的其他部分。

相關文章