現實生活中的分治
分治的思想,顧名思義分而治之。就像古代的王想治理好天下,單單靠他一個人是不夠的,還需要大臣的輔助,把天下劃分為一塊塊區域,分派的下面的人負責,然後下面的人又分派給他們的屬下負責,層層傳遞。
這就是分治,也就是把一個複雜的問題分解成相似的子問題,然後子問題再分子問題,直到問題分的很簡單不必再劃分了。然後層層返回問題的結果,最終上報給王!分治在演算法上有很多應用,類似大資料的MapReduce,歸併演算法、快速排序演算法等。
在JUC中也提供了一個叫Fork/Join的平行計算框架用來處理分治的情況,它類似於單機版的 MapReduce
。
Fork/Join
分治分為兩個階段,第一個階段分解任務,把任務分解為一個個小任務直至小任務可以簡單的計算返回結果。
第二階段合併結果,把每個小任務的結果合併返回得到最終結果。而Fork
就是分解任務,Join
就是合併結果。
Fork/Join框架主要包含兩部分:ForkJoinPool、ForkJoinTask
。
ForkJoinPool
就是治理分治任務的執行緒池。它和在之前的文章提到ThreadPoolExecutor
執行緒池,共同點都是消費者-生產者模式的實現,但是有一些不同。ThreadPoolExecuto
r的執行緒池是隻有一個任務佇列的,而ForkJoinPool
有多個任務佇列。通過ForkJoinPool
的invoke
或submit
或execute
提交任務的時候會根據一定規則分配給不同的任務佇列,並且任務佇列的雙端佇列。
execute 非同步,無返回結果 invoke 同步,有返回結果 (會阻塞) submit 非同步,有返回結果 (Future)
為啥要雙端佇列呢?因為ForkJoinPool
有一個機制,當某個工作執行緒對應消費的任務佇列空閒的時候它會去別的忙的任務佇列的尾部分擔(stealing)任務過來執行(好夥伴啊)。然後那個忙的任務佇列還是頭部出任務給它對應的工作執行緒消費。這樣雙端就井然有序,不會有任務爭搶的情況。
ForkJoinTask
這就是分治任務啦,就等同於我們平日用的Runnable
。它是一個抽象類,核心方法就是fork
和join
。fork
方法用來非同步執行一個子任務,join
方法會阻塞當前執行緒等待子任務返回。
ForkJoinTask
有兩個子類分別是RecursiveAction
和RecursiveTask
。這兩個子類也是抽象類,都是通過遞迴來執行分治任務。每個子類都有抽象方法compute
差別就在於RecursiveAction
的沒有返回值而RecursiveTask
有返回值。
簡單應用
這樣分治思想用遞迴實現的經典案例就是斐波那契數列了。
斐波那契數列:1、1、2、3、5、8、13、21、34、…… 公式 :F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)
public static void main(String[] args) {
ForkJoinPool forkJoinPool = new ForkJoinPool(4); // 最大併發數4
Fibonacci fibonacci = new Fibonacci(20);
long startTime = System.currentTimeMillis();
Integer result = forkJoinPool.invoke(fibonacci);
long endTime = System.currentTimeMillis();
System.out.println("Fork/join sum: " + result + " in " + (endTime - startTime) + " ms.");
}
//以下為官方API文件示例
static class Fibonacci extends RecursiveTask<Integer> {
final int n;
Fibonacci(int n) {
this.n = n;
}
@Override
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();
}
}
複製程式碼
當然你也可以兩個任務都fork
,要注意的是兩個任務都fork
的情況,必須按照f1.fork(),f2.fork(), f2.join(),f1.join()
這樣的順序,不然有效能問題。JDK官方文件有說明,有興趣的可以去研究下。
我是推薦使用invokeAll方法
Fibonacci f1 = new Fibonacci(n - 1);
Fibonacci f2 = new Fibonacci(n - 2);
invokeAll(f1,f2);
return f2.join() + f1.join();
複製程式碼
Method invokeAll (available in multiple versions) performs the most common form of parallel invocation: forking a set of tasks and joining them all.
官方API文件是這樣寫到的,所以平日用invokeAll
就好了。invokeAll會把傳入的任務的第一個交給當前執行緒來執行,其他的任務都fork加入工作佇列,這樣等於利用當前執行緒也執行任務了。以下為invokeAll
原始碼
public static void invokeAll(ForkJoinTask<?>... tasks) {
Throwable ex = null;
int last = tasks.length - 1;
for (int i = last; i >= 0; --i) {
ForkJoinTask<?> t = tasks[i];
if (t == null) {
if (ex == null)
ex = new NullPointerException();
}
else if (i != 0) //除了第一個都fork
t.fork();
else if (t.doInvoke() < NORMAL && ex == null) //留一個自己執行
ex = t.getException();
}
for (int i = 1; i <= last; ++i) {
ForkJoinTask<?> t = tasks[i];
if (t != null) {
if (ex != null)
t.cancel(false);
else if (t.doJoin() < NORMAL)
ex = t.getException();
}
}
if (ex != null)
rethrow(ex);
}
複製程式碼
結語
Fork/Join
就是利用了分治的思想組建的框架,平日裡很多場景都能利用到分治思想。框架的核心ForkJoinPool
,因為含有任務佇列和竊取的特性所以能更好的利用資源。
最後祝大家五一快樂!
如有錯誤歡迎指正! 個人公眾號:yes的練級攻略
有相關面試進階資料等待領取