Java8之Stream流(一)基礎體驗
Java8之Stream流(二)關鍵知識點
Java8之Stream流(三)縮減操作
Java8之Stream流(五)映射流
Java8之Stream流(六)收集
Java8之Stream流(七)流與迭代器
隨著對流API認識的慢慢深入,本章我們要討論的知識點是流API里面的并行流了。在開始討論并行流之前,我先引發一下大家的思考,就你看到這篇文章的時間,你們是不是經常聽到,Intel i7 CPU什么8核16線程,什么Android手機8核4GB這種消息,既然我們是處于一個多核處理器的時代,你們有沒有想過并行地操作數組和集合框架,從而高速地執行我們對數組或者集合的一些操作呢?或許你有想過這個問題,但是因為并行編程比較復雜,所以這些想法還停留在你的腦海當中,又或者你已經在路上了,反正你們就是最棒的(我他媽都這么夸你們了,就不能點個喜歡?),不管如何,在你看到這一篇文章的時候,我將帶你走向并行地操作數組或者集合,當然是使用我們的并行流知識啦。
并行流
并行編程可謂是十分復雜并且很容易出錯的,這估計就是我們絕大部分人的攔腳石。剛好Stream流庫給我們解決了這個問題,在流API庫里面提供了輕松可靠的并行操作。要想并行處理流相當簡單,只需要使用一個并行流就可以了。如第二篇文章中提到的那樣,我們獲取一個并行流是非常簡單的,只需要對流調用一下parallel()就可以獲取到一個并行流了(什么你居然不知道?那么多人看了我的文章,估計你要被他們甩開幾條街了,趕緊回去看吧。),第二種方式就更加簡單了,我們可以使用Collection接口提供給我們parallelStream(),也是可以獲取到一個并行流的。當然,并行操作肯定是需要環境支持的,你搞了一臺一核一線程的小霸王,來跑我的高大上并行流,我也只能慢慢來了。如果你不是小霸王,那我們可以開始這節課的實戰了,先拿上一篇的例子來改一下先,如果你不認真觀察,你都找不出他們的不同之處:
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
}
}
得到結果和上一篇文章的一模一樣。但是因為乘法和加法操作是可以發生在不同的線程里面的,因此這兩個例子,在數據源足夠大的時候,他們的運行的時間,差別相當地大了啊。一般來說,應用到并行流的任何操作都必須是符合縮減操作的三個約束條件,無狀態,不干預,關聯性!因為這三大約束確保在并行流上執行操作的結果和在順序流上執行的結果是相同的。
我們在上一篇講縮減操作的時候,提到了三個reduce(),但是我們只講了兩個,我就不和你們皮了,直接開講剩下的那一個,在并行流里面,你們會發現這個版本的reduce()才是真愛啊!
public interface Stream<T> extends BaseStream<T, Stream<T>> {
//、、、忽略其他無關緊要的元素
<U> U reduce(U identity,
BiFunction<U, ? super T, U> accumulator,
BinaryOperator<U> combiner);
}
在reduce()的這個版本當中,accumulator被稱為累加器, combiner被稱為合成器, combiner定義的函數將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
}
他們得到的結果還是一樣的。
你們可能以為accumulator和combiner執行的操作是相同的,但其實他們是可以不同的,下面的例子,你們要認真看了:假設List里面有三個Integer類型的元素分別為1,2,3。現在的需求是分別讓List里面的每個元素都放大兩倍后,再求積。這個需求的正確答案應該是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流你就可以無往而不利了。如果你還不能理解,就應該繼續往下看了,跟著我的步伐慢慢走:
累加器部分(水平向右)
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執行的操作是不是一定不能相同了。理解這些,對于理解并行流是非常重要的。如果此時的combiner還是和accumulator相同,那么結果是什么樣的呢:請看:
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接口中提供了相應的方法,如果你還沒有記住,回頭再看一下第二篇文章吧。
關于使用并行流的時候,還有一個點需要記住:如果集合中或者數組中的元素是有序的,那么對應的流也是有序的。但是在使用并行流時,有時候流是無序的就能獲得性能上的提升。因為如果流是無序的,那么流的每個部分都可以被單獨的操作,而不需要與其他部分協調,從而提升性能。(又是無狀態,說好的退休了呢)。所以當流操作的順序不重要的時候,可以通過BaseStream接口提供的unordered()方法把流轉換成一個無序流之后,再進行各種操作。
另外一點:forEach()方法不一定會保留并行流的順序,如果在對并行流的每個元素執行操作時,也希望保留順序,那么可以使用forEachOrdered()方法,它的用法和forEach()是一樣的。
因為在發布第一篇文章的時候,大家對forEach的反應比較大,很多人其實對forEach都有想法:比如調試難,等等。借這個機會,我談一談我對for&forEach的看法:我們在訪問一個數組元素的時候,最快的方式肯定是通過索引去訪問的吧,而for循環遍歷的時候就是通過下標進行的,所以效率那是相當的高,但是當我們的數據結構不是數組的時候,比如是鏈表的時候,可想而知,for循環的效率是有多低,但是forEach底層采用的是迭代器的方式,他對數據結構是沒有要求的,不管上層的數據結構是什么,他都能保證高效地執行!因此我的最終答案:如果數據結構是ArrayList這種數據結構,那你可以采用for,但是你的數據結構如果是LinkList那你千萬別再用for,應該果斷采用forEach,因為數據一多起來的,for此時的效率低得可憐,說不定你的機器就癱瘓了。這也是優化的一個小技巧吧,希望能幫助大家。
小結一下
并行流學會了,你的功力,真的就增長了。效率再也不是問題了,基本上關于并行流的方方面面,這篇文章都已經說提到了,但是Stream在JDK中的變化還是挺快的,我一旦發現有什么改動,會最快地更新這篇文章。下一篇我們繼續探索新知識點。