fork英文含義爲叉子也可以理解爲拆分,join英文含義爲加入也可以理解爲彙集,所以forkjoin可以理解爲拆分任務然後將結果匯聚在一起,這種思想和大數據中的MapReduce很像(input --> split --> map --> reduce --> output),所以其可以大致分爲兩步:任務拆分和結果合併。
下面我將以一個demo來演示下forkjoin的基本特質。
package com.dongnaoedu.network.humm.多線程.ForkJoin;
import java.sql.Time;
import java.util.ArrayList;
import java.util.concurrent.*;
/**
* @author Heian
* @time 19/07/28 22:24
* @description: 理解forkjoin(線程池 +任務拆分 )的原理
*/
public class ForkJoinDemo {
//自定義的任務
static ArrayList<String> urls = new ArrayList<String>(){
{
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
}
};
//模擬網絡請求,假設這裏請求耗時爲100毫秒
public static String doRequest(String url) throws InterruptedException{
TimeUnit.MILLISECONDS .sleep (100);
return "訪問的網址:"+url + "\n";
}
//設置我們需要的任務,從多少到多少位一組算一個任務
static class TaskGroup implements Callable<String>{
private int startIndex;
private int endIndex;
public TaskGroup(int startIndex,int endIndex) {
this.startIndex = startIndex;
this.endIndex = endIndex;
}
@Override
public String call() throws Exception {
String sb = "";
for (int i = startIndex-1; i <=endIndex-1 ; i++) {
String s = doRequest (urls.get (i));
sb+=s;
}
return sb;
}
}
/**
* 拆分任務
* @param pageSize 按照多大爲一組,拆分任務
*/
public static void splitTask(int pageSize) throws ExecutionException, InterruptedException {
int size = urls.size ();
int groupCount = size/pageSize + 1; // 9/10 = 0
System.out.println ("任務大小爲:" + size + ",分成" + groupCount + "組來處理");
ExecutorService executorService = Executors.newFixedThreadPool (4);
ArrayList<Future<String>> list = new ArrayList<> ();
long startTime = System.currentTimeMillis ();
/*------------------任務分組邏輯-----------------------*/
//因爲可能最後一組會存在零星的,所以要單獨拿出來
for (int i = 1; i <= groupCount-1; i++) {
int startPageNum = (i-1)*pageSize + 1;//起始頁碼 = (第幾組-1)*pageSize +1 從1開始
int endPageNum = pageSize*i; //截止頁碼 = 第幾組*pageSize
System.out.println (startPageNum + ":" + endPageNum);
Future<String> future = executorService.submit (new TaskGroup (startPageNum, endPageNum));
list.add (future);
}
//零星 最後一組
int startPageNum = (groupCount-1)*pageSize + 1;//起始頁碼 = (第幾組-1)*pageSize +1 從1開始
int endPageNum = size; //截止頁碼 = 第幾組*pageSize
System.out.println (startPageNum + ":" + endPageNum);
Future<String> future = executorService.submit (new TaskGroup (startPageNum, endPageNum));
list.add (future);
for (Future<String> item : list){
//拿到每組任務的返回值(很長的網址拼接)
System.out.println(item.get());//阻塞 每個組的任務會在換行
}
System.out.println ("耗時爲" + (System.currentTimeMillis ()-startTime) + "毫秒");
}
/**
* 我們要做的就是模擬,將我們要訪問的網址,拆分成多組任務,後交給線程池處理
*/
public static void main(String[] args) throws Exception{
splitTask(10);
}
}
首先我這裏定義了一個String類型的字符串(有65個),來模擬網絡請求(每個請求耗時100ms),然後定義了一個線程池(4個線程)去請求這網址,所以這裏可以拆分非10個一組,然後就是7組,然後將每組返回的結果合併返回給結果集。控制檯打印結果如下:
任務大小爲:65,分成7組來處理
1:10
11:20
21:30
31:40
41:50
51:60
61:65
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
訪問的網址:http://www.sina.com
訪問的網址:http://www.baidu.com
耗時爲2024毫秒
而如果要用forkjoin線程框架去實現它又該怎麼做呢?首先ForkJoinTask有兩個子類
- RecursiveAction 一個遞歸無結果的ForkJoinTask(沒有返回值)
- RecursiveTask 一個遞歸有結果的ForkJoinTask(有返回值)
你定義的任務類必須去實現ForkJoinTask的子類,繼而實現它的compute()方法,此方法就是去實現具體的拆分任務的邏輯,可類比我上面的splitTask(int pageSize)方法,這裏我將任務拆分爲2等分,然後兩等分在繼續拆分,知道拆分的邊界符合我的預期<=10,即可,圖解和實示例代碼如下。
package com.dongnaoedu.network.humm.多線程.ForkJoin;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
/**
* @author Heian
* @time 19/07/28 22:24
* @description: 理解forkjoin(線程池 +任務拆分 )的原理
*/
public class ForkJoinTest {
//自定義的任務
static ArrayList<String> urls = new ArrayList<String>(){
{
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
}
};
//模擬網絡請求,假設這裏請求耗時爲100毫秒
public static String doRequest(String url,int index) throws InterruptedException{
TimeUnit.MILLISECONDS .sleep (100);
return index + "-訪問的網址:"+url + "\n";
}
//本質是一個線程池,默認的線程數量:CPU的核數
static ForkJoinPool forkJoinPool = new ForkJoinPool (Runtime.getRuntime().availableProcessors(),//我是4核
ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, false);
/**
* 現在將一個大任務分成多組任務,而分組的邊界有自己設定,直到邊界不能再細分
*/
static class Job extends RecursiveTask<String>{
private List<String> list;//需要拆分的任務
private int start;
private int end;
public Job(List<String> list, int start, int end) {
this.list = list;
this.start = start;
this.end = end;
}
//不斷的拆分任務,直到任務數小於10纔不拆分,
@Override
protected String compute() {
int taskSize = end - start;//得到任務的大小
if (taskSize<=10){
//先把任務拆分好了,在將各組任務執行
System.out.println ("小於10" + Thread.currentThread ().getName ());
String result = "";
for (int i = start; i <end ; i++) {
try {
result += doRequest (urls.get (i),i);
} catch (InterruptedException e) {
e.printStackTrace ();
}
}
return result;
}else {
//拆分任務
int x = (start + end)/2;//將任務分成兩份 奇數:3--> 1,2
System.out.println (x+ Thread.currentThread ().getName ());
//起三個線程去分解這些任務
Job job1 = new Job (urls,start,x);
ForkJoinTask<String> fork = job1.fork ();
Job job2 = new Job (urls,x,end);
ForkJoinTask<String> fork1 = job2.fork ();
//固定寫法 類似於語法
String result = "";
result += job1.join ();
result += job2.join ();
return result;
}
}
}
/**
* 我們要做的就是模擬,將我們要訪問的網址,拆分成多組任務,後交給線程池處理
*/
public static void main(String[] args) throws Exception{
long statrTime = System.currentTimeMillis ();
Job job = new Job (urls,0,urls.size ());
ForkJoinTask<String> result = forkJoinPool.submit (job);
System.out.println (result.get ());
System.out.println ("耗時:" + (System.currentTimeMillis ()-statrTime));
}
}
控制檯信息如下:
32ForkJoinPool-1-worker-1
16ForkJoinPool-1-worker-2
8ForkJoinPool-1-worker-2
小於10ForkJoinPool-1-worker-2
48ForkJoinPool-1-worker-3
40ForkJoinPool-1-worker-3
24ForkJoinPool-1-worker-1
小於10ForkJoinPool-1-worker-1
小於10ForkJoinPool-1-worker-3
小於10ForkJoinPool-1-worker-0
小於10ForkJoinPool-1-worker-3
小於10ForkJoinPool-1-worker-1
56ForkJoinPool-1-worker-0
小於10ForkJoinPool-1-worker-0
小於10ForkJoinPool-1-worker-0
0-訪問的網址:http://www.baidu.com
1-訪問的網址:http://www.sina.com
2-訪問的網址:http://www.baidu.com
3-訪問的網址:http://www.sina.com
4-訪問的網址:http://www.baidu.com
5-訪問的網址:http://www.sina.com
6-訪問的網址:http://www.baidu.com
7-訪問的網址:http://www.sina.com
8-訪問的網址:http://www.baidu.com
9-訪問的網址:http://www.sina.com
10-訪問的網址:http://www.baidu.com
11-訪問的網址:http://www.sina.com
12-訪問的網址:http://www.baidu.com
13-訪問的網址:http://www.sina.com
14-訪問的網址:http://www.baidu.com
15-訪問的網址:http://www.sina.com
16-訪問的網址:http://www.baidu.com
17-訪問的網址:http://www.sina.com
18-訪問的網址:http://www.baidu.com
19-訪問的網址:http://www.sina.com
20-訪問的網址:http://www.baidu.com
21-訪問的網址:http://www.sina.com
22-訪問的網址:http://www.baidu.com
23-訪問的網址:http://www.sina.com
24-訪問的網址:http://www.baidu.com
25-訪問的網址:http://www.sina.com
26-訪問的網址:http://www.baidu.com
27-訪問的網址:http://www.sina.com
28-訪問的網址:http://www.baidu.com
29-訪問的網址:http://www.sina.com
30-訪問的網址:http://www.baidu.com
31-訪問的網址:http://www.sina.com
32-訪問的網址:http://www.baidu.com
33-訪問的網址:http://www.sina.com
34-訪問的網址:http://www.baidu.com
35-訪問的網址:http://www.sina.com
36-訪問的網址:http://www.baidu.com
37-訪問的網址:http://www.sina.com
38-訪問的網址:http://www.baidu.com
39-訪問的網址:http://www.sina.com
40-訪問的網址:http://www.baidu.com
41-訪問的網址:http://www.sina.com
42-訪問的網址:http://www.baidu.com
43-訪問的網址:http://www.sina.com
44-訪問的網址:http://www.baidu.com
45-訪問的網址:http://www.sina.com
46-訪問的網址:http://www.baidu.com
47-訪問的網址:http://www.sina.com
48-訪問的網址:http://www.baidu.com
49-訪問的網址:http://www.sina.com
50-訪問的網址:http://www.baidu.com
51-訪問的網址:http://www.sina.com
52-訪問的網址:http://www.baidu.com
53-訪問的網址:http://www.sina.com
54-訪問的網址:http://www.baidu.com
55-訪問的網址:http://www.sina.com
56-訪問的網址:http://www.baidu.com
57-訪問的網址:http://www.sina.com
58-訪問的網址:http://www.baidu.com
59-訪問的網址:http://www.sina.com
60-訪問的網址:http://www.baidu.com
61-訪問的網址:http://www.sina.com
62-訪問的網址:http://www.baidu.com
63-訪問的網址:http://www.sina.com
64-訪問的網址:http://www.baidu.com
耗時:2559
可以發現按道理線程池處理6500毫秒的任務,理想情況是6500/4=1625,但方法一返回的結果爲2024,方法2返回的結果爲2559,看出對於小任務,ForkJoin並不佔優,畢竟要處理不斷去拆分的邏輯,而且我這裏是定時操作,也就很難看出任務竊取了,這裏只做演示而已,只是要明白每個線程內部都有自己的隊列,並且是雙向隊列FIFO( First in, First out)先進先出,LIFO(Last in, First out)後進先出。
備註:還需研究源碼,這裏暫時不做深入考究,他日有時間在深究。
- Worker線程用LIFO的方法取出任務,後進隊列的任務先取出來(子任務總是後加入隊列,但是需要先執行)
- 當任務隊列爲空,會隨機從其他的worker的隊列中以FIFO拿走一個任務執行(工作竊取:steal work)
- 如果一個Worker線程遇到了join操作,有子任務沒有,會等到這個任務結束。否則直接返回
- 如果一個Worker線程遇到了join操作,有子任務沒有,會等到這個任務結束。否則直接返回