JUC併發編程學習(十四)-任務拆分ForkJoin詳解

分而治之思想

在古代,皇帝要想辦成一件事肯定不會自己親自去動手,而是把任務細分發給下面的大臣,下面的大臣也懶呀,於是把任務繼續分成幾個部分,繼續下發,於是到了最後最終負責的人就完成了一個小功能。上面的領導再把這些結果一層一層彙總,最終返回給皇帝。這就是分而治之的思想。

什麼是ForkJoin

從JDK1.7開始,Java提供ForkJoin框架用於並行執行任務,它的思想就是將一個大任務分割成若干小任務,最終彙總每個小任務的結果得到這個大任務的結果。簡單的理解,ForkJoin是一個可對任務進行拆分,分而治之的類。

ForkJoinPool

既然任務是被逐漸的細化的,那就需要把這些任務存在一個池子裏面,這個池子就是ForkJoinPool,它與其它的ExecutorService區別主要在於它使用“工作竊取“,那什麼是工作竊取呢?

一個大任務會被劃分成無數個小任務,這些任務被分配到不同的隊列,這些隊列有些幹活乾的塊,有些幹得慢。於是幹得快的,一看自己沒任務需要執行了,就去隔壁的隊列裏面拿去任務執行。

ForkJoin特點:工作竊取

A執行隊列中的任務1,2,3,4,5,執行到3
B執行任務,已先執行完成,則幫A執行任務(從A任務的尾部開始竊取任務執行)。
在這裏插入圖片描述

ForkJoinTask

ForkJoinTask就是ForkJoinPool裏面的每一個任務。他主要有兩個子類:RecursiveAction和RecursiveTask。然後通過fork()方法去分配任務執行任務,通過join()方法彙總任務結果。
在這裏插入圖片描述
他有兩個子類,使用這兩個子類都可以實現我們的任務分配和計算。

(1)RecursiveAction 一個遞歸無結果的ForkJoinTask(沒有返回值)

(2)RecursiveTask 一個遞歸有結果的ForkJoinTask(有返回值)

ForkJoinPool由ForkJoinTask數組和ForkJoinWorkerThread數組組成,ForkJoinTask數組負責存放程序提交給ForkJoinPool的任務,而ForkJoinWorkerThread數組負責執行這些任務。

下面我們看看如何使用的。

RecursiveTask

RecursiveTask :有返回結果

第一步:創建MyRecursiveTask類繼承RecursiveTask,重寫compute方法

package com.jp.forkjointest;

import java.util.concurrent.RecursiveTask;

/**
 * @className:
 * @PackageName: com.jp.forkjointest
 * @author: youjp
 * @create: 2020-06-14 13:17
 * @description: TODO
 * 求和計算任務
 * 測試遞歸拆分任務有返回結果。泛型是計算後返回的結果類型
 * @Version: 1.0
 */
public class MyRecursiveTask extends RecursiveTask<Long> {

    //開始值
    private long start;
    //結束值
    private long end;
    //臨界值
    private static final long temp = 10000L;


    public MyRecursiveTask(long start, long end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        //如果任務小的不能拆分,就直接計算
        if (end - start <= temp) {
            long sum = 0;
            for (long i = start; i <= end; i++) {
                sum += i;
            }
            return sum;
        } else {
            //獲取中間值
            long middle = (end + start) / 2;

            //  fork()會不斷的循環
            //第一個任務
            MyRecursiveTask task1 = new MyRecursiveTask(start, middle);
            task1.fork();   //拆分任務,將任務壓入線程隊列
            //第2個任務
            MyRecursiveTask task2 = new MyRecursiveTask(middle + 1, end);
            task2.fork();//拆分任務,將任務壓入線程隊列

            //合併結果
            return task1.join()+task2.join();
        }

    }
}

二、使用ForkJoinPool進行執行
task要通過ForkJoinPool來執行,分割的子任務也會添加到當前工作線程的雙端隊列中,
進入隊列的頭部。當一個工作線程中沒有任務時,會從其他工作線程的隊列尾部獲取一個任務(工作竊取)。

package com.jp.forkjointest;

import com.sun.org.apache.xerces.internal.dom.PSVIAttrNSImpl;

import java.util.concurrent.ForkJoinPool;

/**
 * @className:
 * @PackageName: com.jp.forkjointest
 * @author: youjp
 * @create: 2020-06-14 13:39
 * @description:    計算1到10,0000,0000的和
 *
 * @Version: 1.0
 */
public class Test {

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        //1.創建forkjoinpool
        ForkJoinPool pool=new ForkJoinPool();
        //2.創建ForkJoinTask
        MyRecursiveTask recursiveTask=new MyRecursiveTask(0L,10_0000_0000L);

        //3.ForkJoinPoll對象調用invoke執行,並將ForkJoinTask對象放入ForkJoinPool中
        long sum=  pool.invoke(recursiveTask);
        long endTime = System.currentTimeMillis();
        System.out.println("time:"+(endTime-startTime)+" sum:"+sum);
    }
}

計算結果
在這裏插入圖片描述

性能測試

package com.jp.forkjointest;

import com.sun.org.apache.xerces.internal.dom.PSVIAttrNSImpl;

import java.util.concurrent.ForkJoinPool;
import java.util.stream.LongStream;

/**
 * @className:
 * @PackageName: com.jp.forkjointest
 * @author: youjp
 * @create: 2020-06-14 13:39
 * @description:    計算1到10,0000,0000的和
 *
 * @Version: 1.0
 */
public class Test {

    public static void main(String[] args) {
        //test1();
      test2();
       //test3();
    }


    //普通方法計算求和
    public static void test1(){
        long startTime = System.currentTimeMillis();
        long sum = 0L;
        for (long i = 0L; i <= 10_0000_0000L; i++) {
            sum += i;
        }

        long endTime = System.currentTimeMillis();
        System.out.println("方法1--time:"+(endTime-startTime)+" sum:"+sum);
    }

    //2.使用forkjoin任務拆分並行求和
    public static void test2(){
        long startTime = System.currentTimeMillis();
        //1.創建forkjoinpool
        ForkJoinPool pool=new ForkJoinPool();
        //2.創建ForkJoinTask
        MyRecursiveTask recursiveTask=new MyRecursiveTask(0L,10_0000_0000L);

        //3.ForkJoinPoll對象調用invoke執行,並將ForkJoinTask對象放入ForkJoinPool中
        long sum=  pool.invoke(recursiveTask);
        long endTime = System.currentTimeMillis();
        System.out.println("方法2--time:"+(endTime-startTime)+" sum:"+sum);
    }

    //3. Stream並行流測試
    public static void test3(){
        long startTime=System.currentTimeMillis();
        long sum=LongStream.rangeClosed(0L,10_0000_0000L).parallel().reduce(0L,Long::sum);
        long endTime = System.currentTimeMillis();
        System.out.println("方法3--time:"+(endTime-startTime)+" sum:"+sum);
    }
}

測試了一下,按理來說,strem並行流計算效率最高,然後forkjoin其次,但我的電腦不知道什麼原因,沒有測試出預期接口。。。

RecursiveAction

RecursiveAction在exec後是不會保存返回結果,因此RecursiveAction與RecursiveTask區別在與RecursiveTask是有返回結果而RecursiveAction是沒有返回結果。

例子:控制檯打印結果,遍歷指定目錄(含子目錄)尋找指定txt結尾的文件

public class FindDirsFiles extends RecursiveAction{

    private File path;//當前任務需要搜尋的目錄

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

    public static void main(String [] args){
        try {
            // 用一個 ForkJoinPool 實例調度總任務
            ForkJoinPool pool = new ForkJoinPool();
            FindDirsFiles task = new FindDirsFiles(new File("F:/"));
            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) {
            e.printStackTrace();
        }
    }

	@Override
	protected void compute() {
		
		List<FindDirsFiles> subTasks = new ArrayList<>();//定義子任務個數
		
		File[] files = path.listFiles();//獲取目錄所有文件和目錄
		if(files!=null) {
			for(File file:files) {
				if(file.isDirectory()) {//如果是目錄,就加入子任務中查詢
					subTasks.add(new FindDirsFiles(file));
				}else {
					//遇到文件,檢查
					if(file.getAbsolutePath().endsWith("txt")) {
						System.out.println("文件:"+file.getAbsolutePath());
					}
				}
			}
			if(!subTasks.isEmpty()) {
				for(FindDirsFiles subTask:invokeAll(subTasks)) {
					subTask.join();//等待子任務執行完成
				}
			}
		}
	}
}


ForJoin注意點

使用ForkJoin將相同的計算任務通過多線程的進行執行。從而能提高數據的計算速度。在google的中的大數據處理框架mapreduce就通過類似ForkJoin的思想。通過多線程提高大數據的處理。但是我們需要注意:

  • 使用這種多線程帶來的數據共享問題,在處理結果的合併的時候如果涉及到數據共享的問題,我們儘可能使用JDK爲我們提供的併發容器。
  • 在使用JVM的時候我們要考慮OOM的問題,如果我們的任務處理時間非常耗時,並且處理的數據非常大的時候。會造成OOM。
  • ForkJoin也是通過多線程的方式進行處理任務。那麼我們不得不考慮是否應該使ForkJoin。因爲當數據量不是特別大的時候,我們沒有必要使用ForkJoin。因爲多線程會涉及到上下文的切換。所以數據量不大的時候使用串行比使用多線程快。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章