謹慎使用 Java8 新特性 parallelStream

1. 前言

在說 parallelStream 之前, 一定要了解 Stream 以及它的基本操作

推薦大家看一波之前的文章 解放雙手,Stream 居然還有這波神操作

2. 什麼是 ParallelStream

上文講到的 Java8 Stream 流在執行時候是串行化的, 如果說任務執行的耗時比較長, 可以使用 Stream 的 "兄弟流" ParallelStream

防止誤導, 並非耗時就一定要使用並行, 根據不同的業務場景, 合理的使用即可

parallelStream 是一種並行流, 意思爲處理任務時並行處理, 這裏和併發編程就有了千絲萬縷的關係

前提是硬件支持, 如果單核 CPU, 只會存在併發處理, 而不會並行

這篇文章主要是說明 ParallelStream 其中一個可能爲成爲埋雷的點

項目中業務使用的並行流真的會都並行處理麼?

3. 如何使用 ParallelStream

ParallelStream 在使用上與 Stream 無區別, 本質返回的都是一個流, 只不過底層處理時 根據條件判斷是並行或者是串行

並行流並不會按照原本的順序軌跡執行, 而是 隨機執行, 當然對於這種 forEach 輸出也可以做到順序串行, 但這個不在文章中的重點

4. ForkJoinPool

相信如果在項目中實際使用過並行流的小夥伴, 一定會知道 ForkJoinPool

沒錯, 並行流底層就是使用的 ForkJoinPool, 一種 工作竊取算法線程池

ForkJoinPool 的優勢在於, 可以充分利用多 CPU 的優勢,把一個任務拆分成多個"小任務", 把多個"小任務"放到多個處理器核心上並行執行; 當多個"小任務"執行完成之後, 再將這些執行結果合併起來

5. 並行流的陷阱

5.1 線程安全問題

只要是並行處理, 如果在流程中的操作產生了競態條件, 就會存在線程安全問題

這裏舉個例子進行說明具體問題

public static void main(String[] args) {
    List<Integer> integerList = Lists.newArrayList();
    List<String> strList = Lists.newArrayList();

    int practicalSize = 1000000;

    for (int i = 0; i < practicalSize; i++) {
        strList.add(String.valueOf(i));
    }

    strList.parallelStream().forEach(each -> {
        integerList.add(Integer.parseInt(each));
    });

    log.info("  >>> integerList 預計長度 :: {}", practicalSize);
    log.info("  >>> integerList 實際長度 :: {}", integerList.size());
}
/**
 * >>> integerList 預估長度 :: 1000000
 * >>> integerList 實際長度 :: 211195
 */

上面這段程序運行流程說明如下:

1、創建了兩個 List, 分別是 String、Integer 類型

2、向 strList 插入 1000000 條記錄

3、使用並行流將 strList 中的數據格式化爲 Integer 並添加到 integerList

4、輸出 integerList 預計長度以及實際長度

正常情況下, 我們是希望 integerList 最終輸出 1000000

但是會因爲並行流處理是多線程操作, 所以會導致 ArrayList 的線程不安全

示例中實際長度並不固定, 根據 CPU 的具體處理速度而定

解決方式

如果項目中確實有上述代碼的需求, 可以選擇使用 Vector 類、Colletions 封裝、JUC 類

既然使用了並行處理, 所以對於性能還是有一定要求, 所以這一塊容器偏向於 JUC

5.2 什麼情況下都會並行麼

這個問題, 也就是本文的重點, 小本本做好筆記

首先我們先將能調用並行流的 API 進行羅列

public static void main(String[] args) {
    List<String> stringList = Lists.newArrayList();
    stringList.parallelStream();
    stringList.stream().parallel();
    Stream.of(stringList).parallel();
    ...
}

雖然 API 的調用方式不同, 但是底層都是將 AbstractPipeline 中的 parallel 標識設置爲 true

public final S parallel() {
    sourceStage.parallel = true;
    return (S) this;
}

這就會引出一個問題, 調用這三種不同的並行流 API, 底層是使用的同一個 ForkJoinPool 麼?

首先我們看一下 ForkJoinPool 是如何被初始化的

並行流中使用到的是 ForkJoinPool 內部一個靜態變量屬性

static final ForkJoinPool common;

public static ForkJoinPool commonPool() {
    // assert common != null : "static init error";
    return common;
}

ForkJoinPool 靜態塊負責初始化 common

static {
    // initialize field offsets for CAS etc
    try {
        U = sun.misc.Unsafe.getUnsafe();
        Class<?> k = ForkJoinPool.class;
        CTL = U.objectFieldOffset
                (k.getDeclaredField("ctl"));
        RUNSTATE = U.objectFieldOffset
                (k.getDeclaredField("runState"));
        STEALCOUNTER = U.objectFieldOffset
                (k.getDeclaredField("stealCounter"));
        Class<?> tk = Thread.class;
        PARKBLOCKER = U.objectFieldOffset
                (tk.getDeclaredField("parkBlocker"));
        Class<?> wk = WorkQueue.class;
        QTOP = U.objectFieldOffset
                (wk.getDeclaredField("top"));
        QLOCK = U.objectFieldOffset
                (wk.getDeclaredField("qlock"));
        QSCANSTATE = U.objectFieldOffset
                (wk.getDeclaredField("scanState"));
        QPARKER = U.objectFieldOffset
                (wk.getDeclaredField("parker"));
        QCURRENTSTEAL = U.objectFieldOffset
                (wk.getDeclaredField("currentSteal"));
        QCURRENTJOIN = U.objectFieldOffset
                (wk.getDeclaredField("currentJoin"));
        Class<?> ak = ForkJoinTask[].class;
        ABASE = U.arrayBaseOffset(ak);
        int scale = U.arrayIndexScale(ak);
        if ((scale & (scale - 1)) != 0)
            throw new Error("data type scale not a power of two");
        ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
    } catch (Exception e) {
        throw new Error(e);
    }

    commonMaxSpares = DEFAULT_COMMON_MAX_SPARES;
    defaultForkJoinWorkerThreadFactory =
            new ForkJoinPool.DefaultForkJoinWorkerThreadFactory();
    modifyThreadPermission = new RuntimePermission("modifyThread");

    // 創建ForkJoinPool
    common = java.security.AccessController.doPrivileged
            (new java.security.PrivilegedAction<ForkJoinPool>() {
                public ForkJoinPool run() {
                    return makeCommonPool();
                }
            });
    int par = common.config & SMASK; // report 1 even if threads disabled
    commonParallelism = par > 0 ? par : 1;
}

通過下面初始化代碼可以看到, parallelism、threadFactory、exceptionHandler 可以進行初始個性化配置

private static ForkJoinPool makeCommonPool() {
    int parallelism = -1;
    ForkJoinPool.ForkJoinWorkerThreadFactory factory = null;
    Thread.UncaughtExceptionHandler handler = null;
    try {  // ignore exceptions in accessing/parsing properties
        String pp = System.getProperty
                ("java.util.concurrent.ForkJoinPool.common.parallelism");
        String fp = System.getProperty
                ("java.util.concurrent.ForkJoinPool.common.threadFactory");
        String hp = System.getProperty
                ("java.util.concurrent.ForkJoinPool.common.exceptionHandler");
        if (pp != null)
            parallelism = Integer.parseInt(pp);
        if (fp != null)
            factory = ((ForkJoinPool.ForkJoinWorkerThreadFactory) ClassLoader.
                    getSystemClassLoader().loadClass(fp).newInstance());
        if (hp != null)
            handler = ((Thread.UncaughtExceptionHandler) ClassLoader.
                    getSystemClassLoader().loadClass(hp).newInstance());
    } catch (Exception ignore) {
    }
    if (factory == null) {
        if (System.getSecurityManager() == null)
            factory = defaultForkJoinWorkerThreadFactory;
        else // use security-managed default
            factory = new ForkJoinPool.InnocuousForkJoinWorkerThreadFactory();
    }
    if (parallelism < 0 && // default 1 less than #cores
            (parallelism = Runtime.getRuntime().availableProcessors() - 1) <= 0)
        parallelism = 1;
    if (parallelism > MAX_CAP)
        parallelism = MAX_CAP;
    return new ForkJoinPool(parallelism, factory, handler, LIFO_QUEUE,
            "ForkJoinPool.commonPool-worker-");
}

創建 ForkJoinPool 實例內部線程總數 parallelism 默認爲: 當前運行環境的 CPU 核數 - 1

這一點很重要, 和下面講到的並行流工作方式有很大關係

看到這裏小夥伴應該就會明白了

程序中使用的並行流, 使用的都是 ForkJoinPool 中的靜態變量 common

這裏繼續看本節提出的問題, 項目中使用了並行流的代碼, 真的能夠達到並行麼?

這裏先貼一下測試代碼, 感興趣的小夥伴可以本地也試試

public static void main(String[] args) throws InterruptedException {
    System.out.println(String.format("  >>> 電腦 CPU 並行處理線程數 :: %s, 並行流公共線程池內線程數 :: %s",
            Runtime.getRuntime().availableProcessors(),
            ForkJoinPool.commonPool().getParallelism()));
    List<String> stringList = Lists.newArrayList();
    List<String> stringList2 = Lists.newArrayList();
    for (int i = 0; i < 13; i++) stringList.add(String.valueOf(i));
    for (int i = 0; i < 3; i++) stringList2.add(String.valueOf(i));

    new Thread(() -> {
        stringList.parallelStream().forEach(each -> {
            System.out.println(Thread.currentThread().getName() + " 開始執行 :: " + each);
            try {
                Thread.sleep(6000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }, "子線程-1").start();

    Thread.sleep(1500);

    new Thread(() -> {
        stringList2.parallelStream().forEach(each -> {
            System.out.println(Thread.currentThread().getName() + " :: " + each);
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

    }, "子線程-2").start();
}

爲了模擬項目中正式使用場景, 測試代碼說明如下:

1、"子線程-1" 、"子線程-2" 分別代表項目中兩個不同的業務使用並行流

2、 服務器同時只能保證 12 線程併發, 初始化時公共的 ForkJoinPool 內並行度是 11

3、"子線程-1" 業務比較耗時, 算上執行線程以及線程池內的線程, 併發能跑 12 個任務

4、如果 "子線程-1" 將線程池所有並行跑滿, "子線程-2" 再運行並行流有什麼結果?

跑一下測試程序, 看看會發生什麼事情

這裏說明下運行圖的過程說明:

1、可以看到提交任務的線程也參與到了任務執行中

2、因爲我們公共的 ForkJoinPool 並行是 11, 加上提交任務的線程一共是 12, 但是我們 "子線程-1" 共需執行 13 個任務

3、在 "子線程-1" 中的任務將線程睡眠, 模擬任務耗時, 所以 "子線程-1" 會將公共線程池跑滿的同時, 還會遺留一個任務

4、因爲 "子線程-1" 將任務跑滿, 所以 "子線程-2" 在執行的時候, 不能進行並行處理, 只能依靠提交任務線程執行

5、在 "子線程-1" 的 12 個任務結束運行後, 會再將剩餘的一個任務繼續執行

問題總結

通過上面的測試程序得知: 在項目中使用了並行流真正執行時, 並非一定是並行的

因爲如果項目中其它並行流的任務執行耗時, 會佔據對應資源, 使得最後還是通過主線程執行任務

所以我們在使用並行流之前一定要考慮以下問題:

1、業務場景是否真的需要並行處理?

2、並行處理任務是否是相對獨立? 是否會引起並行間的競態條件?

3、並行處理是否依賴任務的執行順序?

針對這三個問題, 如果業務能夠滿足使用場景, 並且有對應的解決策略, 並行確實是能夠提升相當一部分性能

6. ParallelStream 總結

文章主要描述了什麼是 ParallerStream 是什麼

一種提供了並行計算的流式處理

ParallerStream 底層是通過什麼技術獲得並行計算

ForkJoinPool, 默認並行能力爲 Runtime.getRuntime().availableProcessors() - 1, 可以通過參數指定重寫

並行流存在的一些問題, 其實也可以說是併發編程存在的問題

線程安全性問題及場景是否適用並行處理

總而言之, 並行處理在合適的場景還是可以使用的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章