本章主要對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();
}
}