JAVA併發編程:線程併發工具類Fork-Join原理分析及實戰

1、Fork-Join

  Java下多線程的開發我們可以自己啓用多線程、線程池,還可以使用ForkJoin。 ForkJoin 可以讓我們不去了解諸如 Thread、Runnable 等相關的知識,只要遵循 ForkJoin 的開發模式,就可以寫出很好的多線程併發程序。

2、Fork-Join體現了分而治之

  什麼是分而治之?一種設計思想、策略。十大計算機經典算法:快速排序、堆排序、歸併排序、二分查找、線性查找、深度優先、廣度優先、Dijktra、動態規劃、樸素貝葉斯分類。其中有三個就屬於分而治之的設計思想,分別是快速排序、歸併排序、二分查找。

  分而治之的設計思想是:將一個難以解決的問題,拆分成一些規模較小的問題,以便各個擊破,達到分而治之的目的。

3、Fork-Join原理

  在必要的情況下,將一個大任務,進行拆分(Fork)成若干個小任務,直到不可再拆時,將一個個小任務運算結果進行彙總(Join),最終達到想要的結果,如下圖所示。
​​​​在這裏插入圖片描述

4、Fork-Join實戰

  使用 ForkJoin 框架,必須首先創建一個 ForkJoin 任務。它提供在任務中執行Fork 和 Join 的操作機制,通常我們不直接繼承 ForkjoinTask 類,只需要直接繼承其子類。

  • RecursiveAction,用於沒有返回結果的任務
  • RecursiveTask,用於有返回值的任務

  Task 要通過 ForkJoinPool 來執行,使用 invoke或者submit或者execute 提交,三者的區別是:invoke 是同步執行,調用之後需要等待任務完成,才能執行後面的代碼; submit和execute 是異步執行。 join()和 get() 方法當任務完成的時候返回計算結果。
  任務繼承RecursiveAction或者RecursiveTask類,重寫父類的compute()方法,我們自己的業務寫在compute方法裏即可,首先需要判斷任務是否足夠小,如果足夠小就直接執行任務。如果不足夠小,就必須分割成兩個子任務,每個子任務在調用invokeAll 方法時,又會進入 compute 方法,看看當前子任務是否需要繼續分割成孫任務,如果不需要繼續分割,則執行當前子任務並返回結果。使用 join 方法會等待子任務執行完並得到其結果。

  • Fork/Join的同步用法同時演示返回結果值:統計整形數組中所有元素的和
package cn.lspj.ch2.forkjoin.sum;



import java.util.Random;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

/**
 * 類說明:統計整形數組中所有元素的和
 */
public class SumToForkJoin extends RecursiveTask<Integer> {

    //數組長度
    public static final int ARRAY_LENGTH = 4000;
    /*閾值*/
    private final static int THRESHOLD = ARRAY_LENGTH / 10;
    private int[] array;
    private int fromIndex;
    private int toIndex;

    public SumToForkJoin(int[] array, int fromIndex, int toIndex) {
        this.array = array;
        this.fromIndex = fromIndex;
        this.toIndex = toIndex;
    }

    @Override
    protected Integer compute() {
        // 判斷任務的大小是否合適,當數組長度小於設定的某個閾值時,求該數組的和
        if (toIndex - fromIndex < THRESHOLD) {
            int count = 0;
            for (int i = fromIndex; i < toIndex; i++) {
                count += array[i];
            }
            return count;
        }
        // 數組長度不滿足設定的閾值,繼續拆分數組
        int mid = (fromIndex + toIndex) / 2;
        SumToForkJoin left = new SumToForkJoin(array, fromIndex, mid);
        SumToForkJoin right = new SumToForkJoin(array, mid + 1, toIndex);
        // 調用invokeAll方法繼續執行compute方法
        invokeAll(left, right);
        return left.join() + right.join();
    }

    /**
     * 隨機生成一個數組
     *
     * @return
     */
    private static int[] makeArray() {
        //new一個隨機數發生器
        Random r = new Random();
        int[] result = new int[ARRAY_LENGTH];
        for (int i = 0; i < ARRAY_LENGTH; i++) {
            //用隨機數填充數組
            result[i] = r.nextInt(ARRAY_LENGTH * 3);
        }
        return result;

    }

    public static void main(String[] args) {
        // new 出一個pool實例,用來執行task任務
        ForkJoinPool pool = new ForkJoinPool();
        // 生成一個隨機值的數組
        int[] array = makeArray();
        // new 出一個task任務,交給pool去執行
        SumToForkJoin task = new SumToForkJoin(makeArray(), 0, array.length);
        // 調用pool的invoke方法,同步執行
        pool.invoke(task);
        // 調用task的join方法,獲取數組求和後的值,並輸出
        System.out.println("count=" + task.join());

    }

}

執行結果如下:
在這裏插入圖片描述

  • Fork/Join的異步用法同時演示不要求返回值:遍歷指定目錄(含子目錄)尋找指定類型文件
package cn.lspj.ch2.forkjoin;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;

/**
 *類說明:遍歷指定目錄(含子目錄)找尋指定類型文件
 */
public class FindDirsFilesToForkJoin extends RecursiveAction {

    private File path;

    public FindDirsFilesToForkJoin(File path) {
        this.path = path;
    }

    @Override
    protected void compute() {
        List<FindDirsFilesToForkJoin> subTasks = new ArrayList<>();

        File[] files = path.listFiles();
        if (files!=null){
            for (File file : files) {
                if (file.isDirectory()) {
                    // 對每個子目錄都新建一個子任務。
                    subTasks.add(new FindDirsFilesToForkJoin(file));
                } else {
                    // 遇到文件,檢查。
                    if (file.getAbsolutePath().endsWith("txt")){
                        System.out.println("文件:" + file.getAbsolutePath());
                    }
                }
            }
            if (!subTasks.isEmpty()) {
                // 在當前的 ForkJoinPool 上調度所有的子任務。
                for (FindDirsFilesToForkJoin subTask : invokeAll(subTasks)) {
                    subTask.join();
                }
            }
        }
    }

    public static void main(String [] args){
        try {
            // 用一個 ForkJoinPool 實例調度總任務
            ForkJoinPool pool = new ForkJoinPool();
            FindDirsFilesToForkJoin task = new FindDirsFilesToForkJoin(new File("F:/Program Files"));

            /*異步提交*/
            pool.execute(task);

            /*主線程做自己的業務工作*/
            System.out.println("Task is Running......");
            Thread.sleep(1);
            int otherWork = 0;
            for(int i=0;i<100;i++){
                otherWork = otherWork+i;
            }
            System.out.println("Main Thread done sth......,otherWork="
                    +otherWork);
            task.join();//阻塞方法
            System.out.println("Task end");
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    
}

執行結果如下:
在這裏插入圖片描述
  從執行結果中不難看出,使用execute方法異步調用執行task,主線程不會等待子線程先執行,而是繼續完成主線程的業務工作。


備註:博主微信公衆號,不定期更新文章,歡迎掃碼關注。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章