寫在前面
我們都知道,在開發中有時候要想提高程式的效率,可以使用多執行緒去並行處理。而Java8的速度變快了,這個速度變快的原因中,很重要的一點就是Java8提供了並行方法,它使得我們的程式很容易就能切換成多執行緒,從而更好的利用CPU資源。
下面我們就來簡單學習一下java8中得並行流與序列流。
並行流就是把一個內容分成多個資料塊,並用不同的執行緒分別處理每個資料塊的流。
Java8中將並行進行了優化,我們可以很容易的對資料進行並行操作。Stream API 可以宣告性地通過parallel()與sequential()在並行流與順序(序列)流之間進行切換
Fork/Join框架
在說並行流之前呢,我們首先來來接一下這個Fork/Join框架框架。
Java 7開始引入了一種新的Fork/Join執行緒池,它可以執行一種特殊的任務:把一個大任務拆成多個小任務並行執行。即在必要的情況下,將一個大的任務,進行拆分(fork)成若干個小任務(拆到不可再拆時),再將一個個的小任務運算的結果進行join彙總。
Fork/Join框架與傳統執行緒池的區別
傳統的執行緒池
我們就多執行緒來說吧,所謂的多執行緒就是把我們的任務分配到CPU不同的核上(也就是CPU不同的執行緒上)進行執行,我們以4核CPU為例。如果是傳統執行緒的話,每個任務都有可能會阻塞,因為每個執行緒什麼時候執行是由CPU時間片給他分配的執行權決定的,當這個時間片用完了以後,CPU會剝奪他的執行權,然後交給其他的執行緒去執行,這時就有可能出現阻塞的情況。即4核CPU我們可以看成4個執行緒,有可能其中倆執行緒中的一個任務阻塞造成後面的任務排隊得不到執行,而另外兩個沒有阻塞的執行緒,則順利執行完處於空閒狀態了,這種有的執行緒在阻塞執行緒裡的任務得不到執行,而別的不阻塞的執行緒空閒沒有任務可以執行的狀態,就造成了CPU資源的浪費,這樣就會大大影響我們程式的執行效率。
Fork/Join框架
是把一個大任務拆分成若干個小任務,然後把這些小任務都壓入到對應的執行緒中,也就是把這些小任務都壓入到對應的CPU中(預設CPU有幾核就有幾個執行緒),然後形成一個個的執行緒佇列。
Fork/Join任務的原理:判斷一個任務是否足夠小,如果是,直接計算,否則,就分拆成幾個小任務分別計算。這個過程可以反覆“裂變”成一系列小任務。
Fork/Join框架會將任務分發給執行緒池中的工作執行緒。Fork/Join框架的獨特之處在於它使用“工作竊取”(work-stealing)演算法。完成自己的工作而處於空閒的工作執行緒,能夠從其他扔處於忙碌狀態的工作執行緒中竊取等待執行的任務,每個工作執行緒都有自己的工作佇列,這是使用雙端佇列(dequeue)來實現的。執行緒執行任務是從佇列頭部開始執行的,而處於空閒狀態的執行緒,在竊取別的執行緒的任務的時候,是從被竊取執行緒的等待佇列的隊尾開始竊取的。這種情況下,就不會出現空閒的執行緒浪費CPU資源,因為一旦空閒便會去竊取任務執行。沒有資源浪費,減少了執行緒的等待時間,所以效率就高,就提升了效能。
下面我們舉個例子:如果要計算一個超大陣列的和,最簡單的做法是用一個迴圈在一個執行緒內完成。還有一種方法,可以把陣列拆成兩部分,分別計算,最後加起來就是最終結果,這樣可以用兩個執行緒並行執行,如果拆成兩部分還是很大,我們還可以繼續拆,用4個執行緒並行執行,這種即使用Fork/Join對大資料進行並行求和。
Fork/Join框架的使用
下面我們來寫測試類演示一下:實現數的累加操作,比如說計算1到100億的和。
我們要編寫一個類繼承RecursiveTask類,並重寫compute()方法。
package com.cqq.java8.parallel;
import java.util.concurrent.RecursiveTask;
/**
* @Description:遞迴進行拆分
* @date 2021/3/14 7:55
*/
public class ForkJoinCalculate extends RecursiveTask<Long> {
private Long start;
private Long end;
public ForkJoinCalculate(Long start, Long end) {
this.start = start;
this.end = end;
}
//臨界值,當大於臨界值的時候就一直拆分,小於臨界值就不再進行拆分了
private static final long THREASHOLD = 100000000L;
//重寫compute方法
@Override
protected Long compute() {
long length = end - start;
if(length <= THREASHOLD){//到臨界值就不能再拆了
long sum = 0;
for (Long i = start; i <= end; i++) {
sum += i;
}
return sum;
}else{//不到臨界值就進行拆分
long middle = (end + start);
ForkJoinCalculate left = new ForkJoinCalculate(start,middle);
//拆分子任務,同時壓入執行緒佇列
left.fork();
ForkJoinCalculate right = new ForkJoinCalculate(middle+1,end);
right.fork();
//拆完之後,合併,把fork()之後的結果得一個個合併,即累加總和
return left.join()+right.join();
}
}
}
測試方法
package com.cqq.java8;
import com.cqq.java8.parallel.ForkJoinCalculate;
import org.junit.Test;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
/**
* @Description:
* @date 2021/3/14 8:17
*/
public class TestForkJoin {
@Test
public void test01(){
//開始時間
Instant start = Instant.now();
//ForkJoin的執行需要一個ForkJoinPool的支援
ForkJoinPool pool = new ForkJoinPool();
ForkJoinTask<Long> task = new ForkJoinCalculate((long) 0,100000000L);
Long invoke = pool.invoke(task);
System.out.println(invoke);
//結束時間
Instant end = Instant.now();
//計算一下時間用 耗時多少
System.out.println(Duration.between(start,end).toMillis());
}
//一個普通for迴圈即傳統的單執行緒的測試類 與Fork/Join的執行結果做對比
@Test
public void test02(){
Instant start = Instant.now();
long sum = 0L;
for (long i = 0; i < 100000000L; i++) {
sum += i;
}
System.out.println(sum);
Instant end = Instant.now();
System.out.println(Duration.between(start,end).toMillis());
}
}
測試結果
類加和 | ForkJoin耗時 | 傳統單執行緒耗時 |
---|---|---|
1-1億 | 521 | 85 |
1-10億 | 241 | 363 |
1-100億 | 1103 | 2431 |
從測試結果可以看出,當任務量不大時,傳統單執行緒耗時短,任務達到一定量時ForkJoin的效能就很好了,因為在任務量不大時,拆分任務也要耗時,所以總的執行時間就比較長。說明,多執行緒也是要在合適的時候用才能提升效能。
Java8中的並行流
在Java 8中我們用的是parallel()方法,對並行流進行了優化。但是實際上底層還是用的Fork/Join框架。
@Test
public void test03(){
Instant start = Instant.now();
//順序流
long reduce = LongStream.rangeClosed(0, 100000000L)
.reduce(0, Long::sum);
//使用parallel()並行流
OptionalLong reduce1 = LongStream.rangeClosed(0, 100000000L)
.parallel()//並行
.reduce(Long::sum);
Instant end = Instant.now();
System.out.println(Duration.between(start,end).toMillis());
}
Java8 中不僅僅對程式碼進行了優化,而且效率也大大提升。