Java多線程——ForkJoin併發框架


本章主要對ForkJoin併發框架進行學習,主要內容分爲三個部分:

  • ForkJoin併發框架的淺談
  • ForkJoin併發編程的兩個實例
  • ForkJoinPool線程池的常用方法說明

1.ForkJoin併發框架的淺談

1.1.Fork和Join

ForkJoin併發框架Fork=分解 + Join=合併

ForkJoin併發框架是Java 7提供的一個用於並行執行任務的框架,是一個把大任務分割(Fork)成若干個小任務,最終彙總(Join)每個小任務結果後得到大任務結果的框架。

例如:計算1+2+…1000000000,可以將其分割(Fork)爲100000個小任務,每個任務計算10000個數據的相加,最終彙總(Join)這100000個小任務的計算結果進行合併,得到計算結果。


1.2.工作竊取算法

ForkJoin併發框架是採取工作竊取(Work-Stealing)算法實現的。

工作竊取算法:某個線程從其他線程的工作隊列中竊取任務來執行。可以形象的用下面的圖表示:
這裏寫圖片描述
下面來詳細說明工作竊取算法(模擬):

  • 有一個較大的任務劃分成了10個小任務。
  • 這10個小任務在一個大小爲2的線程池中執行。
  • 線程池中的2個核心線程,每個線程的隊列中有5個任務。
  • 線程1的任務都很簡單,所以它很快就將5個任務執行完畢。
  • 線程2的任務都很複雜,當線程1執行完5個任務時,他才執行了3個任務。
  • 這時,線程1不會空閒,而且竊取線程2的等待隊列中的任務(從末端開始竊取)來執行。
  • 當線程2的隊列中也沒有了任務之後,線程1和線程2才空閒。

優缺點

  • 整體上,這種竊取算法,提高了線程利用率
  • 爲了減少竊取任務線程和被竊取任務線程之間的競爭,通常會使用雙端隊列。
  • 存在兩個線程共同競爭同一個任務的可能,例如雙端隊列中只有一個任務時。

1.3.編程思想

ForkJoin併發框架應用了兩種十分重要的編程思想:

  • 分而治之
  • 遞歸

1.4.ForkJoin的主要類

ForkJoin併發框架的主要類包括:

  • ForkJoinPool:ForkJoin線程池,實現了ExecutorService接口和工作竊取算法,用於線程調度與管理。
  • ForkJoinTask:ForkJoin任務,提供了fork()方法join()方法。通常不直接使用,而是使用以下子類:
    • RecursiveAction:無返回值的任務,通常用於只fork不join的情形。
    • RecursiveTask:有返回值的任務,通常用於fork+join的情形。

1.5.ForkJoin的兩類用法

根據ForkJoinTask的兩種類型,可以將ForkJoin併發框架劃分爲兩種用法:

  • only fork:遞歸劃分子任務,分別執行,但是並不需要合併各自的執行結果。
  • fork+join:遞歸劃分子任務,分別執行,然後遞歸合併計算結果。

only fork的示意圖:
這裏寫圖片描述

fork+join的示意圖:
這裏寫圖片描述


2.實例編碼

2.2.RecursiveAction實例編碼

場景說明:

  • 真實場景:專網A內的數據庫DB1上存儲着100萬條數據,需要通過數據交換服務發送到專網B的數據庫DB2上。
  • 原來的古老做法:由於帶寬和服務器性能等限制,每次發送的數據不能超過5000條。所以將這100萬數據按照5000條一組進行分組,然後每組都通過一個線程進行發送。但是不知道什麼原因,總之DB2中會經常出現重複數據。
  • 新的做法:根據ForkJoin框架編程思想,將這100萬數據按照閾值THRESHOLD進行子任務劃分,然後依次發送。

重點分析:

  • 這個場景只是將100萬數據分組進行分發,並不需要再將分組合並,所以屬於上述的第一種only fork用法。
  • 爲了模擬對數據的接收,定義了一個ConcurrentLinkedQueue用於存儲接收的數據。
  • 如果DB2最終的數據量與DB1的數據量一直,則表明數據發送成功。
  • 注意如何根據閾值THRESHOLD計算分組(fork())。
  • 注意遞歸分組的用法。

代碼:


/**
 * <p>ForkJoin框架實例1-RecursiveAction-無返回值-數據交換</p>
 * <p>數據交換:專網A內的數據庫DB1上有100萬數據,需要通過數據交換服務發送到專網B的數據庫DB2上。
 * 1.原來的做法:將這100萬數據按照5000條一組進行分組,然後每組都通過一個線程進行發送。不知道什麼原因,總之經常會出現重複發送的數據。
 * 2.新的做法:根據ForkJoin框架編程思想,將這100萬數據按照閾值THRESHOLD進行子任務劃分,然後依次發送。</p>
 *
 * @author hanchao 2018/4/15 19:26
 **/
public class RecursiveActionDemo {
    private static final Logger LOGGER = Logger.getLogger(RecursiveActionDemo.class);
    //模擬數據庫DB2
    static ConcurrentLinkedQueue DB2 = new ConcurrentLinkedQueue();

    /**
     * <p>定義一個數據交換任務,繼承自RecursiveAction,用於發送數據交換的JSON數據</p>
     *
     * @author hanchao 2018/4/15 19:28
     **/
    static class DataExchangeTask extends RecursiveAction {

        //閾值=5000
        private static final int THRESHOLD = 5000;
        //開始索引
        private int start;
        //結束索引
        private int end;
        //交換的數據
        List<String> list;

        public DataExchangeTask(int start, int end, List<String> list) {
            this.start = start;
            this.end = end;
            this.list = list;
        }

        @Override
        protected void compute() {
            //如果當前任務數量在閾值範圍內,則發送數據
            if (end - start < THRESHOLD) {
                //發送Json數據
                try {
                    sendJsonDate(this.list);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                //如果當前任務數量超出閾值,則進行任務拆分
                int middle = (start + end) / 2;
                //左邊的子任務
                DataExchangeTask left = new DataExchangeTask(start, middle, list);
                //右邊的子任務
                DataExchangeTask right = new DataExchangeTask(middle, end, list);
                //並行執行兩個“小任務”
                left.fork();
                right.fork();
            }
        }

        /**
         * <p>發送數據</p>
         *
         * @author hanchao 2018/4/15 20:04
         **/
        private void sendJsonDate(List<String> list) throws InterruptedException {
            //遍歷
            for (int i = start; i < end; i++) {
                //每個元素都插入到DB2中 ==> 模擬數據發送到DB2
                DB2.add(list.get(i));
            }
            //假定每次發送耗時1ms
            Thread.sleep(1);
        }
    }

    /**
     * <p>模擬從數據庫中查詢數據並形成JSON個是的數據</p>
     *
     * @author hanchao 2018/4/15 20:21
     **/
    static void queryDataToJson(List list) {
        //隨機獲取100萬~110萬個數據
        int count = RandomUtils.nextInt(1000000, 1100000);
        for (int i = 0; i < count; i++) {
            list.add("{\"id\":\"" + UUID.randomUUID() + "\"}");
        }
    }

    /**
     * <p>RecursiveAction-無返回值:可以看成只有fork沒有join</p>
     *
     * @author hanchao 2018/4/15 19:26
     **/
    public static void main(String[] args) throws InterruptedException {
        //從數據庫中獲取所有需要交換的數據
        List dataList = new ArrayList<String>();
        queryDataToJson(dataList);
        int count = dataList.size();
        LOGGER.info("1.從DB1中讀取數據並存放到List中,共計讀取了" + count + "條數據.");

        //DB2的數據量
        LOGGER.info("2.開始時,DB2中的數據量:" + DB2.size());

        LOGGER.info("3.通過ForkJoin框架進行子任務劃分,併發送數據");
        //定義一個ForkJoin線程池
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        //定義一個可分解的任務
        DataExchangeTask dataExchangeTask = new DataExchangeTask(0, count, dataList);
        //向ForkJoin線程池提交任務
        forkJoinPool.submit(dataExchangeTask);
        //線程阻塞,等待所有任務完成
        forkJoinPool.awaitTermination(5, TimeUnit.SECONDS);
        //任務完成之後關閉線程池
        forkJoinPool.shutdown();

        //查詢最終傳輸的數據量
        LOGGER.info("4.結束時,DB2中的數據量:" + DB2.size());
        //查詢其中一條數據
        LOGGER.info("5.查詢其中一條數據:" + DB2.peek());
    }
}

運行結果:

2018-04-17 23:38:05 INFO - 1.從DB1中讀取數據並存放到List中,共計讀取了1037606條數據.
2018-04-17 23:38:05 INFO - 2.開始時,DB2中的數據量:0
2018-04-17 23:38:05 INFO - 3.通過ForkJoin框架進行子任務劃分,併發送數據
2018-04-17 23:38:10 INFO - 4.結束時,DB2中的數據量:1037606
2018-04-17 23:38:10 INFO - 5.查詢其中一條數據:{"id":"85bc8085-4836-41e3-b7f4-d80f38d8f0fe"}

運行結果,表明發送數據成功。


2.2.RecursiveTask實例編碼

場景說明:

  • 連續整數求和:N,N+1,N+2,N+3…N+MAX
  • 舉例:1+2+3+4+…+1000000000=500000000500000000
  • 第一種方式:單線程計算
  • 第二種方式:ForkJoin併發計算

重點分析:

  • 需要將1000000000個數據分成若干組,分別求和,然後合併計算結果。所以屬於上述的第二種fork+join用法。
  • 注意如何根據閾值THRESHOLD計算分組(fork())。
  • 注意遞歸fork()的用法。
  • 注意遞歸join()的用法。

代碼:

/**
 * <p>ForkJoin框架-RecursiveTask-有返回值-超大集合分割計算</p>
 * <p>計算N,N+1,N+2....N+Max的和</p>
 * <p>第一種方式:單線程計算</p>
 * <p>第二種方式:ForkJoin併發計算</p>
 * @author hanchao 2018/4/15 21:31
 **/
public class RecursiveTaskDemo {
    private static final Logger LOGGER = Logger.getLogger(RecursiveTaskDemo.class);

    /**
     * <p>超大集合計算任務-泛型類</p>
     *
     * @author hanchao 2018/4/15 21:34
     **/
    static class LargeSetComputeTask extends RecursiveTask<Long> {

        //閾值
        private static final int THRESHOLD = 100000;
        private int start;//開始下標
        private int end;//結束下標

        public LargeSetComputeTask(int start, int end) {
            this.start = start;
            this.end = end;
        }

        @Override
        protected Long compute() {
            //如果當前任務的計算量在閾值範圍內,則直接進行計算
            if (end - start < THRESHOLD) {
                return computeByUnit();
            } else {//如果當前任務的計算量超出閾值範圍,則進行計算任務拆分
                //計算中間索引
                int middle = (start + end) / 2;
                //定義子任務-迭代思想
                LargeSetComputeTask left = new LargeSetComputeTask(start, middle);
                LargeSetComputeTask right = new LargeSetComputeTask(middle, end);
                //劃分子任務-fork
                left.fork();
                right.fork();
                //合併計算結果-join
                return left.join() + right.join();
            }
        }

        /**
         * <p>最小計算單元進行計算</p>
         *
         * @author hanchao 2018/4/15 21:39
         **/
        private long computeByUnit() {
            long sum = 0L;
            for (int i = start; i < end; i++) {
                sum += i;
            }
            return sum;
        }
    }

    /**
     * <p>ForkJoin框架-RecursiveTask</p>
     * <p>1.有返回值:可用Future接口進行結果獲取</p>
     * <p>2.RecursiveTask需要fork和join並用</p>
     *
     * @author hanchao 2018/4/15 21:44
     **/
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //計算(0+1+2+3+1000000000)*2的結果
        int count = 1000000001;

        //第一種方式:單線程計算
        long start1 = System.currentTimeMillis();
        LOGGER.info("1.第一種計算方式--單線程計算");
        long result = 0L;
        for (long i = 0; i < count; i++) {
            result += i;
        }
        LOGGER.info("1.計算結果:" + result + ",用時:" + (System.currentTimeMillis() - start1) + "ms.\n");

        //通過ForkJoin框架進行子任務計算
        long start2 = System.currentTimeMillis();
        LOGGER.info("2.第二種計算方式--ForkJoin框架計算");
        //定義ForkJoinPool線程池
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        //定義計算任務
        LargeSetComputeTask computeTask = new LargeSetComputeTask(0, count);
        //提交計算任務
        Future<Long> future = forkJoinPool.submit(computeTask);
        //執行完任務關閉線程池
        forkJoinPool.shutdown();
        //輸出計算結果:
        LOGGER.info("2.計算結果:" + future.get() + ",用時:" + (System.currentTimeMillis() - start2) + "ms.");
    }
}

運行結果:

2018-04-17 23:52:45 INFO - 1.第一種計算方式--單線程計算
2018-04-17 23:52:45 INFO - 1.計算結果:500000000500000000,用時:338ms.

2018-04-17 23:52:45 INFO - 2.第二種計算方式--ForkJoin框架計算
2018-04-17 23:52:45 INFO - 2.計算結果:500000000500000000,用時:213ms.

運行結果說明兩種方式計算結果都正確,fork+join效率高。


3.ForkJoinPool的常用方法

上面的兩個實例對ForkJoin併發框架的編程方式進行了入門介紹。

爲了更加全面的瞭解ForkJoin併發框架,下面對ForkJoinPool的常用方法進行簡單的羅列:

/**
 * <p>ForkJoin-ForkJoinPool的方法學習</p>
 *
 * @author hanchao 2018/4/15 22:12
 **/
public class ForkJoinPoolBasicDemo {
    /**
     * <p>ForkJoin-ForkJoinPool的方法學習</p>
     *
     * @author hanchao 2018/4/15 22:14
     **/
    public static void main(String[] args) {
        //構造函數
        //無參:並行級別=Runtime.getRuntime.availableProcessors();
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        //指定並行級別
        ForkJoinPool forkJoinPool1 = new ForkJoinPool(4);

        //提交任務(返回計算情況)
        //ForkJoinTask<V> implements Future<V>, Serializable
        //提交Runnable任務
        Runnable runnable = null;
        forkJoinPool.submit(runnable);
        //提交Runnable + result任務
        Integer result = null;
        Future<Integer> future2 = forkJoinPool.submit(runnable, result);
        //提交Callable<V>任務
        Callable<Integer> callable = null;
        Future<Integer> future3 = forkJoinPool.submit(callable);
        //提交ForkJoinTask<V>任務
        ForkJoinTask<Integer> forkJoinTask = null;
        Future<Integer> future4 = forkJoinPool.submit(forkJoinTask);
        //提交RecursiveAction任務(RecursiveAction extends ForkJoinTask<Void>)
        RecursiveAction recursiveAction = null;
        forkJoinPool.submit(recursiveAction);
        //提交RecursiveTask<V>任務(RecursiveTask<V> extends ForkJoinTask<V>)
        RecursiveTask<Integer> recursiveTask = null;
        Future<Integer> future6 = forkJoinPool.submit(recursiveTask);

        //提交任務(不返回計算情況)
        //提交Runnable任務
        Runnable runnable1 = null;
        forkJoinPool.execute(runnable1);
        //提交ForkJoinTask<V>任務
        ForkJoinTask<Integer> forkJoinTask1 = null;
        forkJoinPool.execute(forkJoinTask);
        //提交RecursiveAction任務(RecursiveAction extends ForkJoinTask<Void>)
        RecursiveAction recursiveAction1 = null;
        forkJoinPool.execute(recursiveAction);
        //提交RecursiveTask<V>任務(RecursiveTask<V> extends ForkJoinTask<V>)
        RecursiveTask<Integer> recursiveTask1 = null;
        forkJoinPool.execute(recursiveTask);

        //提交任務(返回計算結果)
        //提交ForkJoinTask<V>任務
        ForkJoinTask<Integer> forkJoinTask2 = null;
        Integer result1 = forkJoinPool.invoke(forkJoinTask);
        //提交RecursiveAction任務(RecursiveAction extends ForkJoinTask<Void>)
        RecursiveAction recursiveAction2 = null;
        forkJoinPool.invoke(recursiveAction);
        //提交RecursiveTask<V>任務(RecursiveTask<V> extends ForkJoinTask<V>)
        RecursiveTask<Integer> recursiveTask2 = null;
        Integer result3 = forkJoinPool.invoke(recursiveTask);

        //提交任務集
        //獲取最先計算完成的-阻塞
        List<Callable<Integer>> callableList = new ArrayList<Callable<Integer>>();
        try {
            Integer result4 = forkJoinPool.invokeAny(callableList);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        //獲取最先計算完成的-阻塞-可超時
        try {
            Integer result5 = forkJoinPool.invokeAny(callableList, 1, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
        //所有任務計算完成之後,返回結果-阻塞
        List<Future<Integer>> futureList = forkJoinPool.invokeAll(callableList);
        //所有任務計算完成之後,返回結果-阻塞-可超時
        try {
            List<Future<Integer>> futureList1 = forkJoinPool.invokeAll(callableList, 1, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //是否正在終止
        forkJoinPool.isTerminating();
        //是否終止
        forkJoinPool.isTerminated();
        try {
            //等待終止
            forkJoinPool.awaitTermination(1, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //是否休眠
        forkJoinPool.isQuiescent();
        //等待休眠
        forkJoinPool.awaitQuiescence(1, TimeUnit.SECONDS);

        //存在等待執行的子任務
        forkJoinPool.hasQueuedSubmissions();

        //是否是FIFO模式
        boolean asyncMode = forkJoinPool.getAsyncMode();
        //獲取當前活躍線程數
        int activeThreadCount = forkJoinPool.getActiveThreadCount();
        //獲取線程池並行級別
        int parallelism = forkJoinPool.getParallelism();
        //獲取工作線程數量
        int poolSize = forkJoinPool.getPoolSize();
        //獲取等待執行的子任務數量
        int queuedSubmissionCount = forkJoinPool.getQueuedSubmissionCount();
        //獲取等待執行的任務數量
        long queuedTaskCount = forkJoinPool.getQueuedTaskCount();
        //獲取非阻塞的活動線程數量
        int runningThreadCount = forkJoinPool.getRunningThreadCount();
        //獲取竊取線程數量
        long stealCount = forkJoinPool.getStealCount();
        //獲取工作線程工廠
        ForkJoinPool.ForkJoinWorkerThreadFactory threadFactory = forkJoinPool.getFactory();
        //獲取未捕獲異常處理器
        Thread.UncaughtExceptionHandler handler = forkJoinPool.getUncaughtExceptionHandler();

        //關閉線程池
        forkJoinPool.isShutdown();
        forkJoinPool.shutdown();
        forkJoinPool.shutdownNow();
    }
}
                    
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章