分而治之思想
在古代,皇帝要想辦成一件事肯定不會自己親自去動手,而是把任務細分發給下面的大臣,下面的大臣也懶呀,於是把任務繼續分成幾個部分,繼續下發,於是到了最後最終負責的人就完成了一個小功能。上面的領導再把這些結果一層一層彙總,最終返回給皇帝。這就是分而治之的思想。
什麼是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。因爲多線程會涉及到上下文的切換。所以數據量不大的時候使用串行比使用多線程快。