爲什麼用 Fork/Join ?
對於簡單的並行任務,你可以通過“線程池+Future”的方案來解決;如果任務之間有聚合關係,無論是AND聚合還是OR聚合,都可以通過CompletableFuture來解決;而批量的並行任務,則可以通過CompletionService來解決。這幾種方案基本上能夠覆蓋日常工作中的併發場景了,但還是不夠全面,因爲還有一種“分治”的任務模型沒有覆蓋到。
分治,顧名思義,即分而治之,是一種解決複雜問題的思維方法和模式;具體來講,指的是把一個複雜的問題分解成多個相似的子問題,然後再把子問題分 解成更小的子問題,直到子問題簡單到可以直接求解。
如何用Fork/Join 並行計算框架計算斐波那契數列
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
public class forkjoin {
public static void main(String[] args) {
//創建分治任務線程池
ForkJoinPool fjp = new ForkJoinPool(4);
//創建分治任務
Fibonacci fib = new Fibonacci(30);
//啓動分治任務
Long start_time = System.currentTimeMillis();
Integer result = fjp.invoke(fib);
//輸出結果
Long end_time = System.currentTimeMillis();
Long compute_time = end_time - start_time;
System.out.println("result: "+result);
System.out.println("forkjoin compute_time: "+ compute_time);
}
//遞歸任務
static class Fibonacci extends RecursiveTask<Integer>{
final int n;
Fibonacci(int n){this.n = n;}
protected Integer compute(){
if(n<=1)
return n;
Fibonacci f1 = new Fibonacci(n-1);
//創建⼦任務
f1.fork();
Fibonacci f2 = new Fibonacci(n-2);
//等待⼦任務結果,併合並結果
return f2.compute()+f1.join();
}
}
}
普通單線程計算斐波那契數列
public class fibonacci {
public static void main(String[] args) {
Fibonacci fib = new Fibonacci(30);
Long start_time = System.currentTimeMillis();
Integer result = fib.compute();
Long end_time = System.currentTimeMillis();
Long compute_time = end_time - start_time;
System.out.println("result: "+result);
System.out.println("compute_time: "+ compute_time);
}
//遞歸任務
static class Fibonacci {
final int n;
Fibonacci(int n){
this.n = n;
}
protected Integer compute(){
//終止條件
if(n<=1)
return n;
//創建⼦任務
Fibonacci f1 = new Fibonacci(n-1);
Fibonacci f2 = new Fibonacci(n-2);
//等待⼦任務結果,併合並結果
return f2.compute()+f1.compute();
}
}
}
分別執行,會發現普通計算的方式會更快,說明forkjoin在調度方面會有很大的性能消耗。
在終止條件下加上一條休眠語句,使每次計算都要持續設定時間以上,再對比兩種方式的速度。
Fibonacci fib = new Fibonacci(5);
//終止條件
if(n<=1) {
if(n<=1) {
try {
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
return n;
}
}
可以發現並行的計算方式的執行速度,與設定的的線程數量有關,比如第五個斐波那契數列的值,需要分解爲8次計算任務,每次至少需要8秒。普通計算方式需要8秒,並行計算方式在線程數爲4的情況下,執行時間爲2秒;線程數爲8的情況下,執行時間爲1秒。實際執行速度應與CPU的核數有關,即如果CPU只有4核,就算設置爲8線程,最快速度也只有2秒(示例中執行時間爲1秒是因爲採用線程休眠來模擬該線程的總處理時間,實際該線程在休眠期間並不消耗計算資源)
使用Fork/Join需要注意的地方
在使用Fork/Join時,需要注意出現工作線程不工作的事情。下面從不同的使用方式來分析: fork() 和 compute() 。
在示例中,下面這句語句是分解任務以及合併任務的關鍵
f1.fork();
//等待⼦任務結果,併合並結果
return f2.compute()+f1.join();
主線程分配了一個任務給子線程,同時自己也執行一次計算任務。
方式1: a.fork(); b.fork(); b.join(); a.join();
f1.fork();
f2.fork();
return f2.join()+f1.join();
這種使用方式內部做了很多優化,目的都是爲了避免出現工作線程只分配任務而不執行任務的情況。
方式2:invokeAll(a,b)
invokeAll(f1, f2);
return f2.join()+f1.join();
invokeAll的N個任務中,其中N-1個任務會使用fork()交給其它線程執行,但是,它還會留一個任務自己執行,這樣,就充分利用了線程池,保證沒有空閒的不幹活的線程。
方式3:a.fork(); b.fork(); a.join(); b.join(); (錯誤的使用方式,不建議使用)
f1.fork();
f2.fork();
return f1.join()+f2.join();
這種使用方式會出現工作線程只分配任務給子線程,自己卻不執行任務的情況。比如某個任務可以分爲四個子任務,線程a把任務分給線程b線程c,而線程b線程c又繼續分給自己的子線程(b分解爲b1和b2 / c分解爲c1和c2),總共需要7個線程,但是abc卻不參與計算。而第一種使用方法,Java內部做了優化。
總結
用兩次fork()在join的時候,需要用這樣的順序:a.fork(); b.fork(); b.join(); a.join();這個要求在JDK官方文檔裏有說明。
建議使用fork() 和 compute() ,這樣可以避免避免性能問題,且更容易理解。該用法可理解爲在主線程中使用fork分解一次任務,同時該線程執行一次compute 計算,如果該compute計算還需要分解任務,則繼續fork() 和 compute(),直到滿足終止條件直接返回。因此主線程會不斷分解任務,直到任務無法分解並計算該任務返回計算結果。即主線程的處理任務是分解n/2 次任務以及執行一次無法分解的任務。
參考 :
https://www.liaoxuefeng.com/article/001493522711597674607c7f4f346628a76145477e2ff82000
https://time.geekbang.org/discuss/detail/90130/