Java8之Stream流(一)基礎(chǔ)體驗
Java8之Stream流(二)關(guān)鍵知識點
Java8之Stream流(三)縮減操作
Java8之Stream流(五)映射流
Java8之Stream流(六)收集
Java8之Stream流(七)流與迭代器
隨著對流API認識的慢慢深入生巡,本章我們要討論的知識點是流API里面的并行流了糟描。在開始討論并行流之前,我先引發(fā)一下大家的思考,就你看到這篇文章的時間疆柔,你們是不是經(jīng)常聽到晴音,Intel i7 CPU什么8核16線程灰粮,什么Android手機8核4GB這種消息哎榴,既然我們是處于一個多核處理器的時代寝杖,你們有沒有想過并行地操作數(shù)組和集合框架违施,從而高速地執(zhí)行我們對數(shù)組或者集合的一些操作呢?或許你有想過這個問題瑟幕,但是因為并行編程比較復(fù)雜磕蒲,所以這些想法還停留在你的腦海當中留潦,又或者你已經(jīng)在路上了,反正你們就是最棒的(我他媽都這么夸你們了辣往,就不能點個喜歡兔院?),不管如何站削,在你看到這一篇文章的時候坊萝,我將帶你走向并行地操作數(shù)組或者集合,當然是使用我們的并行流知識啦许起。
并行流
并行編程可謂是十分復(fù)雜并且很容易出錯的十偶,這估計就是我們絕大部分人的攔腳石。剛好Stream流庫給我們解決了這個問題园细,在流API庫里面提供了輕松可靠的并行操作惦积。要想并行處理流相當簡單,只需要使用一個并行流就可以了猛频。如第二篇文章中提到的那樣狮崩,我們獲取一個并行流是非常簡單的,只需要對流調(diào)用一下parallel()就可以獲取到一個并行流了(什么你居然不知道鹿寻?那么多人看了我的文章睦柴,估計你要被他們甩開幾條街了,趕緊回去看吧毡熏。)坦敌,第二種方式就更加簡單了,我們可以使用Collection接口提供給我們parallelStream(),也是可以獲取到一個并行流的招刹。當然恬试,并行操作肯定是需要環(huán)境支持的,你搞了一臺一核一線程的小霸王疯暑,來跑我的高大上并行流,我也只能慢慢來了哑舒。如果你不是小霸王妇拯,那我們可以開始這節(jié)課的實戰(zhàn)了,先拿上一篇的例子來改一下先洗鸵,如果你不認真觀察,你都找不出他們的不同之處:
public class Main {
public static void main(String[] args) {
learnStream();
}
private static void learnStream() {
List<Integer> lists = new ArrayList<>();
lists.add(1);
lists.add(2);
lists.add(3);
lists.add(4);
lists.add(5);
lists.add(6);
Optional<Integer> sum = lists.parallelStream().reduce((a, b) -> a + b);//這里把stream()換成了parallelStream()
if (sum.isPresent()) System.out.println("list的總和為:" + sum.get());//21
//<====> lists.stream().reduce((a, b) -> a + b).ifPresent(System.out::println);
Integer sum2 = lists.stream().reduce(0, (a, b) -> a + b);//21
System.out.println("list的總和為:" + sum2);
Optional<Integer> product = lists.stream().reduce((a, b) -> a * b);
if (product.isPresent()) System.out.println("list的積為:" + product.get());//720
Integer product2 = lists.parallelStream().reduce(1, (a, b) -> a * b);//這里把stream()換成了parallelStream()
System.out.println("list的積為:" + product2);//720
}
}
得到結(jié)果和上一篇文章的一模一樣膘滨。但是因為乘法和加法操作是可以發(fā)生在不同的線程里面的甘凭,因此這兩個例子,在數(shù)據(jù)源足夠大的時候火邓,他們的運行的時間丹弱,差別相當?shù)卮罅税〉虑恕R话銇碚f,應(yīng)用到并行流的任何操作都必須是符合縮減操作的三個約束條件躲胳,無狀態(tài)蜓洪,不干預(yù),關(guān)聯(lián)性坯苹!因為這三大約束確保在并行流上執(zhí)行操作的結(jié)果和在順序流上執(zhí)行的結(jié)果是相同的隆檀。
我們在上一篇講縮減操作的時候,提到了三個reduce(),但是我們只講了兩個粹湃,我就不和你們皮了恐仑,直接開講剩下的那一個,在并行流里面为鳄,你們會發(fā)現(xiàn)這個版本的reduce()才是真愛熬账!
public interface Stream<T> extends BaseStream<T, Stream<T>> {
//济赎、鉴逞、、忽略其他無關(guān)緊要的元素
<U> U reduce(U identity,
BiFunction<U, ? super T, U> accumulator,
BinaryOperator<U> combiner);
}
在reduce()的這個版本當中司训,accumulator被稱為累加器构捡, combiner被稱為合成器, combiner定義的函數(shù)將accumulator提到的兩個值合并起來壳猜,因此勾徽,我們可以把上面的那個例子改成:
private static void reduce3th() {
List<Integer> lists = new ArrayList<>();
lists.add(1);
lists.add(2);
lists.add(3);
lists.add(4);
lists.add(5);
lists.add(6);
Integer product2 = lists.parallelStream().reduce(1, (a, b) -> a * b,
(a, b) -> a * b);
System.out.println("list的積為:" + product2);//720
}
他們得到的結(jié)果還是一樣的。
你們可能以為accumulator和combiner執(zhí)行的操作是相同的统扳,但其實他們是可以不同的喘帚,下面的例子,你們要認真看了:假設(shè)List里面有三個Integer類型的元素分別為1咒钟,2吹由,3。現(xiàn)在的需求是分別讓List里面的每個元素都放大兩倍后朱嘴,再求積倾鲫。這個需求的正確答案應(yīng)該是48;
private static void reduce3th() {
List<Integer> lists = new ArrayList<>();
lists.add(1);
lists.add(2);
lists.add(3);
Integer product = lists.parallelStream().reduce(1, (a, b) -> a * (b * 2),
(a, b) -> a * b);
System.out.println("product:" + product);//48
}
累加器部分是將兩個元素分別放大兩倍后,再相乘,合并器萍嬉,是將兩個部分相乘乌昔!如果能理解這里,恭喜你壤追,你的技能有相當大的長進了磕道!估計Stream流你就可以無往而不利了。如果你還不能理解行冰,就應(yīng)該繼續(xù)往下看了溺蕉,跟著我的步伐慢慢走:
累加器部分(水平向右)
accumulator
-----------------------------?
thread-1: 1 * 1 * 2 = 2 | 合并器方向(豎直向下)
thread-2: 1 * 2 * 2 = 4 | combiner
thread-3: 1 * 3 * 2 = 6 | 因此最終的答案是2 * 4 * 6 = 48(沒毛擦尕ぁ)
ˇ
注:水平方向最前面的1就是identity的值
此時,accumulator和combiner執(zhí)行的操作是不是一定不能相同了焙贷。理解這些撵割,對于理解并行流是非常重要的。如果此時的combiner還是和accumulator相同辙芍,那么結(jié)果是什么樣的呢:請看:
private static void reduce3th() {
List<Integer> lists = new ArrayList<>();
lists.add(1);
lists.add(2);
lists.add(3);
Integer product = lists.parallelStream().reduce(1, (a, b) -> a * (b * 2),
(a, b) -> a * b * 2 );
System.out.println("product:" + product);//192
}
192這個答案是怎么來的啡彬?
累加器部分(水平向右)
accumulator
-----------------------------?
thread-1: 1 * 1 * 2 = 2 | 合并器方向(豎直向下)
thread-2: 1 * 2 * 2 = 4 * 2 | combiner
thread-3: 1 * 3 * 2 = 6 * 2 | 因此最終的答案是2 * ( 4 * 2 ) * (6 * 2) = 192(沒毛病)
ˇ
注:水平方向最前面的1就是identity的值
順序流&并行流&無序流之間的切換操作
對于這三種流的切換故硅,在BaseStream接口中提供了相應(yīng)的方法庶灿,如果你還沒有記住,回頭再看一下第二篇文章吧吃衅。
關(guān)于使用并行流的時候往踢,還有一個點需要記住:如果集合中或者數(shù)組中的元素是有序的徘层,那么對應(yīng)的流也是有序的峻呕。但是在使用并行流時,有時候流是無序的就能獲得性能上的提升趣效。因為如果流是無序的瘦癌,那么流的每個部分都可以被單獨的操作,而不需要與其他部分協(xié)調(diào)跷敬,從而提升性能讯私。(又是無狀態(tài),說好的退休了呢)西傀。所以當流操作的順序不重要的時候斤寇,可以通過BaseStream接口提供的unordered()方法把流轉(zhuǎn)換成一個無序流之后,再進行各種操作拥褂。
另外一點:forEach()方法不一定會保留并行流的順序娘锁,如果在對并行流的每個元素執(zhí)行操作時,也希望保留順序肿仑,那么可以使用forEachOrdered()方法致盟,它的用法和forEach()是一樣的。
因為在發(fā)布第一篇文章的時候尤慰,大家對forEach的反應(yīng)比較大,很多人其實對forEach都有想法:比如調(diào)試難雷蹂,等等伟端。借這個機會,我談一談我對for&forEach的看法:我們在訪問一個數(shù)組元素的時候匪煌,最快的方式肯定是通過索引去訪問的吧责蝠,而for循環(huán)遍歷的時候就是通過下標進行的党巾,所以效率那是相當?shù)母撸钱斘覀兊臄?shù)據(jù)結(jié)構(gòu)不是數(shù)組的時候霜医,比如是鏈表的時候齿拂,可想而知,for循環(huán)的效率是有多低肴敛,但是forEach底層采用的是迭代器的方式署海,他對數(shù)據(jù)結(jié)構(gòu)是沒有要求的,不管上層的數(shù)據(jù)結(jié)構(gòu)是什么医男,他都能保證高效地執(zhí)行砸狞!因此我的最終答案:如果數(shù)據(jù)結(jié)構(gòu)是ArrayList這種數(shù)據(jù)結(jié)構(gòu),那你可以采用for,但是你的數(shù)據(jù)結(jié)構(gòu)如果是LinkList那你千萬別再用for,應(yīng)該果斷采用forEach,因為數(shù)據(jù)一多起來的镀梭,for此時的效率低得可憐刀森,說不定你的機器就癱瘓了。這也是優(yōu)化的一個小技巧吧报账,希望能幫助大家研底。
小結(jié)一下
并行流學會了,你的功力透罢,真的就增長了榜晦。效率再也不是問題了,基本上關(guān)于并行流的方方面面琐凭,這篇文章都已經(jīng)說提到了芽隆,但是Stream在JDK中的變化還是挺快的,我一旦發(fā)現(xiàn)有什么改動统屈,會最快地更新這篇文章胚吁。下一篇我們繼續(xù)探索新知識點。