面試官:說說你對Fork/Join的並行計算框架的瞭解?

現實生活中的分治

分治的思想,顧名思義分而治之。就像古代的王想治理好天下,單單靠他一我的是不夠的,還須要大臣的輔助,把天下劃分爲一塊塊區域,分派的下面的人負責,而後下面的人又分派給他們的屬下負責,層層傳遞。 面試

這就是分治,也就是把一個複雜的問題分解成類似的子問題,而後子問題再分子問題,直到問題分的很簡單沒必要再劃分了。而後層層返回問題的結果,最終上報給王!

分治在算法上有不少應用,相似大數據的MapReduce,歸併算法、快速排序算法等。 在JUC中也提供了一個叫Fork/Join的並行計算框架用來處理分治的狀況,它相似於單機版的 MapReduce算法

Fork/Join

分治分爲兩個階段,第一個階段分解任務,把任務分解爲一個個小任務直至小任務能夠簡單的計算返回結果bash

第二階段合併結果,把每一個小任務的結果合併返回獲得最終結果。而Fork就是分解任務,Join就是合併結果。併發

Fork/Join框架主要包含兩部分:ForkJoinPool、ForkJoinTask框架

ForkJoinPool

就是治理分治任務的線程池。它和在以前的文章提到ThreadPoolExecutor線程池,共同點都是消費者-生產者模式的實現,可是有一些不一樣。ThreadPoolExecutor的線程池是隻有一個任務隊列的,而ForkJoinPool有多個任務隊列。經過ForkJoinPoolinvokesubmitexecute提交任務的時候會根據必定規則分配給不一樣的任務隊列,而且任務隊列的雙端隊列。 異步

execute 異步,無返回結果 invoke 同步,有返回結果 (會阻塞) submit 異步,有返回結果 (Future)ide

爲啥要雙端隊列呢?由於ForkJoinPool有一個機制,當某個工做線程對應消費的任務隊列空閒的時候它會去別的忙的任務隊列的尾部分擔(stealing)任務過來執行(好夥伴啊)。而後那個忙的任務隊列仍是頭部出任務給它對應的工做線程消費。這樣雙端就井井有理,不會有任務爭搶的狀況。 post

ForkJoinTask

這就是分治任務啦,就等同於咱們平日用的Runnable。它是一個抽象類,核心方法就是forkjoinfork方法用來異步執行一個子任務,join方法會阻塞當前線程等待子任務返回。性能

ForkJoinTask有兩個子類分別是RecursiveActionRecursiveTask。這兩個子類也是抽象類,都是經過遞歸來執行分治任務。每一個子類都有抽象方法compute差異就在於RecursiveAction的沒有返回值而RecursiveTask有返回值。大數據

簡單應用

這樣分治思想用遞歸實現的經典案例就是斐波那契數列了。

斐波那契數列:一、一、二、三、五、八、1三、2一、3四、…… 公式 :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的練級攻略

有相關面試進階資料等待領取

相關文章
相關標籤/搜索