本文討論:
- 串行和并行的執(zhí)行效率區(qū)別
- 并行帶來的一些問題
- 什么時候使用并行
ps:主要參考資料來自Effective Java 第三版
先來看一個關(guān)于性能提升的例子(來自effective java),是用來計算小于n的素數(shù)個數(shù),機(jī)器是一個6核cpu(i5-8400)。
肉眼可見的性能得到了提升。
@Test
public void test() throws InterruptedException {
long start = System.currentTimeMillis();
pi(10000000);
long end = System.currentTimeMillis();
System.out.println("串行時間:" + (end - start) / 1000);
start = System.currentTimeMillis();
piParallel(10000000);
end = System.currentTimeMillis();
System.out.println("并行時間:" + (end - start) / 1000);
}
static long pi(long n) {
return LongStream.rangeClosed(2, n).mapToObj(BigInteger::valueOf).filter(i -> i.isProbablePrime(50)).count();
}
static long piParallel(long n) {
return LongStream.rangeClosed(2, n).parallel().mapToObj(BigInteger::valueOf).filter(i -> i.isProbablePrime(50)).count();
}
// console
串行時間:31
并行時間:6
并行處理看起來非常美好,但是,真的如此嗎?先看一個官方給出的反面教材,下面的一段代碼,每次執(zhí)行都會得到不同的結(jié)果
https://docs.oracle.com/javase/tutorial/collections/streams/parallelism.html
線程安全集合
Integer[] intArray = {1, 2, 3, 4, 5, 6, 7, 8};
List<Integer> listOfIntegers = new ArrayList<>(Arrays.asList(intArray));
List<Integer> parallelStorage = Collections.synchronizedList(new ArrayList<>());
listOfIntegers.parallelStream()
// Don't do this! It uses a stateful lambda expression.
.map(e -> {
parallelStorage.add(e);
return e;
})
.forEachOrdered(e -> System.out.print(e + " "));
System.out.println();
parallelStorage.stream().forEachOrdered(e -> System.out.print(e + " "));
// console
1 2 3 4 5 6 7 8
4 2 8 3 7 1 5 6
// console
1 2 3 4 5 6 7 8
6 5 1 4 2 3 7 8
線程不安全集合
Integer[] intArray = {1, 2, 3, 4, 5, 6, 7, 8};
List<Integer> listOfIntegers = new ArrayList<>(Arrays.asList(intArray));
List<Integer> parallelStorage = Lists.newArrayList();
listOfIntegers.parallelStream()
// Don't do this! It uses a stateful lambda expression.
.map(e -> {
parallelStorage.add(e);
return e;
})
.forEachOrdered(e -> System.out.print(e + " "));
System.out.println();
parallelStorage.stream().forEachOrdered(e -> System.out.print(e + " "));
// console
1 2 3 4 5 6 7 8
4 8 7 1 5 2
// console
1 2 3 4 5 6 7 8
2 8 3 5 1 6 4 7
// console
1 2 3 4 5 6 7 8
null 4 6 1 5 8 7 2
從結(jié)果來看,有時候元素會變少,有時候甚至還出現(xiàn)了null。原因也非常簡單:ArrayList不是線程安全的
使用并行流引出的第一個問題:
- 結(jié)果不確定性
官方文檔有直接給出建議:
The lambda expression e -> { parallelStorage.add(e); return e; } is a stateful lambda expression. Its result can vary every time the code is run.
有狀態(tài)的lambda表達(dá)式每次運(yùn)行都會出現(xiàn)不同的結(jié)果。所以我們應(yīng)該避免使用有狀態(tài)的代碼。
我們再看下面一個例子(也是來自effective java)
先來一個串行計算,和預(yù)計一樣,很好的完了代碼,并打印出了相應(yīng)結(jié)果
@Test
public void test() {
primes().map(p -> BigInteger.valueOf(2).pow(p.intValueExact()).subtract(ONE))
.filter(mersenne -> mersenne.isProbablePrime(50))
.limit(10)
.forEach(System.out::println);
}
static Stream<BigInteger> primes() {
return Stream.iterate(BigInteger.valueOf(2), BigInteger::nextProbablePrime);
}
// console
3
7
31
127
8191
131071
524287
2147483647
2305843009213693951
618970019642690137449562111
再把他改成并行的試試,程序不動了
這里發(fā)生了什么?簡而言之,流類庫不知道如何并行化此管道并且啟發(fā)式失?。╤euristics fail)。 即使在最好的情況下,如果源來自 Stream.iterate 方法,或者使用中間操作 limit 方法,并行化管道也不太可能提高其性能
簡而言之limit()方法是個stateful中間操作,并行處理的時候并不知道該如何并行
@Test
public void test() {
primes().map(p -> BigInteger.valueOf(2).pow(p.intValueExact()).subtract(ONE))
.filter(mersenne -> mersenne.isProbablePrime(50))
.limit(10)
.forEach(System.out::println);
}
再看一些其他的例子,對比一下串行,并行,和傳統(tǒng)寫法的性能差別
List<Long> list = Lists.newArrayList();
for (long i = 0; i < 1000000; i++) {
list.add(i);
}
long start = System.currentTimeMillis();
System.out.println("串行:" + list.stream().filter(n -> n % 3 == 0).mapToLong(n -> n).sum());
long end = System.currentTimeMillis();
System.out.println("串行時間:" + (end - start) + "ms");
start = System.currentTimeMillis();
System.out.println("并行:" + list.stream().parallel().filter(n -> n % 3 == 0).mapToLong(n -> n).sum());
end = System.currentTimeMillis();
System.out.println("并行時間:" + (end - start) + "ms");
start = System.currentTimeMillis();
long sum = 0;
for (long l : list) {
if (l % 3 == 0) {
sum += l;
}
}
System.out.println("傳統(tǒng):" + sum);
end = System.currentTimeMillis();
System.out.println("傳統(tǒng)時間:" + (end - start) + "ms");
// console
串行:166666833333
串行時間:49ms
并行:166666833333
并行時間:23ms
傳統(tǒng):166666833333
傳統(tǒng)時間:9ms
最后貼幾個effective java里面關(guān)于并行流建議
- 通常,并行性帶來的性能收益在 ArrayList、HashMap、HashSet 和 ConcurrentHashMap 實(shí)例、數(shù)組、int 類型范圍和 long 類型的范圍的流上最好
- 并行化一個流不僅會導(dǎo)致糟糕的性能,包括活性失?。╨iveness failures);它會導(dǎo)致不正確的結(jié)果和不可預(yù)知的行為 (安全故障)
- 在適當(dāng)?shù)那闆r下,只需向流管道添加一個 parallel 方法調(diào)用,就可以實(shí)現(xiàn)處理器內(nèi)核數(shù)量的近似線性加速
最重要的一個建議就是
總之,甚至不要嘗試并行化流管道,除非你有充分的理由相信它將保持計算的正確性并提高其速度。不恰當(dāng)?shù)夭⑿谢鞯拇鷥r可能是程序失敗或性能災(zāi)難。如果您認(rèn)為并行性是合理的,那么請確保您的代碼在并行運(yùn)行時保持正確,并在實(shí)際情況下進(jìn)行仔細(xì)的性能度量。如果您的代碼是正確的,并且這些實(shí)驗(yàn)證實(shí)了您對性能提高的懷疑,那么并且只有這樣才能在生產(chǎn)代碼中并行化流。