- 線程池+Future: 簡單並行任務
- CompletableFuture: 聚合任務
- CompletionService: 批量並行任務
- Fork/Join: 分治
分治任務模型
- 分治任務模型分爲兩個階段:任務分解 + 結果合併
- 任務分解 :將任務迭代地分解爲子任務,直至子任務可以 直接計算 出結果
- 任務和分解後的子任務具有 相似性 (算法相同,只是計算的數據規模不同,往往採用 遞歸 算法)
- 結果合併 :逐層合併子任務的執行結果,直至獲得最終結果
Fork/Join
概述
- Fork/Join是 並行計算 的框架,主要用來支持分治任務模型,Fork對應任務分解,Join對應結果合併
- Fork/Join框架包含兩部分:分治任務 ForkJoinTask + 分治任務線程池 ForkJoinPool
- 類似於Runnable + ThreadPoolExecutor
- ForkJoinTask最核心的方法是 fork 和 join
- fork:異步地執行一個子任務
- join:阻塞當前線程,等待子任務的執行結果
- ForkJoinTask有兩個子類:RecursiveAction + RecursiveTask
- Recursive:通過 遞歸 的方式來處理分治任務
- RecursiveAction.compute:沒有返回值
- RecursiveTask.compute:有返回值
簡單使用
// 遞歸任務 @AllArgsConstructor class Fibonacci extends RecursiveTask<Integer> { private final int n; @Override protected Integer compute() { if (n <= 1) { return n; } // 創建子任務 Fibonacci f1 = new Fibonacci(n - 1); f1.fork(); Fibonacci f2 = new Fibonacci(n - 2); f2.fork(); // 等待子任務結果併合並 return f1.join() + f2.join(); } } // 創建分治任務線程池 ForkJoinPool pool = new ForkJoinPool(4); // 創建分治任務 Fibonacci fibonacci = new Fibonacci(30); // 啓動分治任務 System.out.println(pool.invoke(fibonacci)); // 832040
ForkJoinPool的工作原理
- Fork/Join並行計算的核心組件是ForkJoinPool
- ThreadPoolExecutor本質上是 生產者-消費者 模式的實現
- 內部有一個 任務隊列 ,該任務隊列是生產者和消費者通信的媒介
- ThreadPoolExecutor可以有 多個工作線程 ,但這些工作線程都 共享一個任務隊列
- ForkJoinPool本質上也是 生產者-消費者 模式的實現,但更加 智能
- ThreadPoolExecutor內部只有一個任務隊列,而ForkJoinPool內部有 多個任務隊列
- 當通過invoke或submit 提交任務 時,ForkJoinPool會根據一定的 路由規則 把任務提交到一個任務隊列
- 如果任務在執行過程中 創建子任務 ,那麼該子任務被會提交到 工作線程對應的任務隊列 中
- ForkJoinPool支持 任務竊取 ,如果工作線程空閒了,那麼它會竊取其他任務隊列裏的任務
- ForkJoinPool的任務隊列是 雙端隊列
- 工作線程 正常獲取任務 和 竊取任務 分別從任務隊列 不同的端 消費,避免不必要的數據競爭
統計單詞數量
@AllArgsConstructor class MapReduce extends RecursiveTask<Map<String, Long>> { private String[] fc; private int start; private int end; @Override protected Map<String, Long> compute() { if (end - start == 1) { return calc(fc[start]); } else { int mid = (start + end) / 2; // 前半部分數據fork一個遞歸任務 MapReduce mr1 = new MapReduce(fc, start, mid); mr1.fork(); // 後半部分數據在當前任務中遞歸處理 MapReduce mr2 = new MapReduce(fc, mid, end); // 計算子任務,返回合併的結果 return merge(mr2.compute(), mr1.join()); } } // 統計單詞數量 private Map<String, Long> calc(String line) { Map<String, Long> result = new HashMap<>(); String[] words = line.split("\\s+"); for (String word : words) { if (result.containsKey(word)) { result.put(word, result.get(word) + 1); } else { result.put(word, 1L); } } return result; } // 合併結果 private Map<String, Long> merge(Map<String, Long> r1, Map<String, Long> r2) { Map<String, Long> result = new HashMap<>(r1); r2.forEach((word, count) -> { if (result.containsKey(word)) { result.put(word, result.get(word) + count); } else { result.put(word, count); } }); return result; } } String[] fc = {"hello world", "hello me", "hello fork", "hello join", "fork join in world"}; ForkJoinPool pool = new ForkJoinPool(3); MapReduce mapReduce = new MapReduce(fc, 0, fc.length); Map<String, Long> result = pool.invoke(mapReduce); result.forEach((word, count) -> System.out.println(word + " : " + count));
小結
- Fork/Join並行計算框架主要解決的是 分治任務 ,分治的核心思想是 分而治之
- Fork/Join並行計算框架的核心組件是 ForkJoinPool ,支持 任務竊取 ,讓所有線程的工作量基本 均衡
- Java 1.8提供的 Stream API 裏的並行流是以ForkJoinPool爲基礎的
- 默認情況下,所有並行流計算都 共享一個ForkJoinPool ,該共享的ForkJoinPool的線程數是 CPU核數
- 如果存在 IO密集型 的並行流計算,那可能會因爲一個很慢的IO計算而影響整個系統的 性能
- 因此,建議 用不同的ForkJoinPool執行不同類型的計算任務
從去年到現在,我根據市場技術棧的需求,整理了一套JAVA的最新教程,如果你現在也在學習Java,在入門學習Java的過程當中缺乏系統的學習教程,你可以加我的Java學習交流羣:【94687,1227】,獲取,羣裏還有學習手冊,面試題,開發工具,PDF文檔教程,可以自行下載。