摘要:ForkJoin執行緒池是將任務分割為子任務,有可能子任務還是很大,還需要進一步拆解,最終得到足夠小的任務。
本文分享自華為雲社群《ForkJoin執行緒池的學習和思考》,作者:breakDraw。
ForkJoin執行緒池在常規的java書籍裡還是提到比較少的,畢竟是java8引入的產物。
首先這裡簡單解釋一下forkJoin的運作原理, 本質上有點像歸併計算。
- 他會將提交大任務按照一定規則拆解(fork)成多個小任務
- 當任務小到一定程度時,就會執行計算
- 執行完成時會和其他的小任務進行合併(join), 逐步將所有小結果合成一個大結果。
可以看這個forkJoinTask的實現虛擬碼,即如果想使用forkJoin併發執行任務,需要自己把任務繼承RecursiveTask,作為forkJoin池的submit物件:
public class ForkJoinTask extends RecursiveTask<任務引數> { public ReckonTask(任務引數) { } @Override protected File compute() { if(根據任務引數判斷任務是否足夠小) { 計算,返回 } else { 拆分成子任務1和子任務2 任務1.fork(); 任務2.fork(); 結果1 = 任務1.join(); 結果2 = 任務2.join(); 返回結果1+結果2; } } }
然後實際上整個forkjoin的細節非常多,這裡我通過給自己提好幾個問題,來逐步理解forkJoin的原理:
Q: forkJoin中各個執行緒是如何獲取那些小任務的呢?
A:他是通過工作密取的方式獲取。(java併發那本書裡提到過工作密取workSteal,原來是用在這了)
- 假設我們給forkJoin設定3個工作執行緒,那麼就會有3個工作佇列, 注意,這個佇列是雙端佇列。
- 每當執行任務時,如果不滿足小任務的條件,他會fork出2個子任務,並push進自己的工作佇列中。
- 每個工作執行緒不斷取自己隊頭的任務執行。
- 關鍵點:如果自己佇列裡沒有資料,則會從其他佇列的隊尾取資料。
Q: fork時具體發生了什麼?
A:是一個非同步的操作, 就是向當前執行緒佇列中新增這個fork出來任務,能放進去的話就返回,不會等待。
注意,預設fork出的任務是先預設給自己的。 當自己做不完時,才可能被別人取走!
Q: join是什麼含義?什麼時候做的?
A:見實現forkJoin任務介面時的程式碼:
可以看到時每次fork完之後, 通過join,來獲取子task的結果,獲取到之後,再合併計算,返回結果。
Q: join這個阻塞過程是怎麼做的?如果把執行緒掛起,那這個執行緒豈不是無法工作了?
A:首先,之前fork時,新的子任務已經被放入佇列了。
每個子任務都有一個任務狀態。
當呼叫該子任務的join時, 會迴圈判斷他的狀態
如果這個子任務狀態未完成, 則從自身佇列或其他人的佇列中取出新的任務執行,因此進入了下一層的exec()操作。
如果發現子任務狀態更新為了完成(這個更新動作可能是自己執行緒完成的,也可能是別的執行緒完成的,反正這個任務的狀態實現了同步和可見), 則將結果返回給上層。
因此join的本質是一個遞迴的過程, 任務沒完成的話,他就取其他任務繼續遞迴往下執行。
更詳細的可以看這個連結fork+join過程詳細解讀
Q: forkJoin存放任務的時候,怎麼保證不會出現併發問題?比如同時往隊尾插入的話
A:
- n個工作執行緒是通過陣列存放的(即有一個工作執行緒陣列)
- sun.misc.Unsafe操作類直接基於作業系統控制層在硬體層面上進行原子操作,它是ForkJoinPool高效效能的一大保證,類似的程式設計思路還體現在java.util.concurrent包中相當規模的類功能實現中。
Q: forkJoin應用在哪嗎?
A:java8 stream的parallel併發功能就是基於forkJoin做的, parallelStream實現的forkJoin拆解任務和執行任務的介面, 預設用機器所有CPU數量的forkJoin執行緒池。
如果需要限制執行緒數量,可以用
new forkJoin(執行緒數).submit(()->(list.stream().parallel().map()…)); 即可