和阿里P8大佬面試互懟了半小時(shí)的Fork/Join原理
只聽到P8大佬不急不慢問道:談?wù)剬?duì)JDK并發(fā)工具的認(rèn)識(shí)?
我開始仔細(xì)梳理多年的并發(fā)八股文經(jīng)驗(yàn),道:
線程池、Future、CompletableFuture和CompletionService這些并發(fā)工具都是幫助SE站在任務(wù)角度解決并發(fā)問題,而非糾結(jié)于線程之間協(xié)作的細(xì)節(jié),比如線程之間如何實(shí)現(xiàn)等待、通知。
- 簡(jiǎn)單并行任務(wù)
線程池+Future 組合拳
- 任務(wù)間有聚合關(guān)系
AND、OR聚合,CompletableFuture 一招鮮
- 批量的并行任務(wù)
CompletionService 一把梭
并發(fā)編程可分為三個(gè)層面問題:分工、協(xié)作、互斥。
當(dāng)關(guān)注于任務(wù)時(shí),你會(huì)發(fā)現(xiàn)你的視角已躍出并發(fā)編程細(xì)節(jié),而使用現(xiàn)實(shí)世界思維模式,類比現(xiàn)實(shí)世界的分工,其實(shí)線程池、Future、CompletableFuture和CompletionService都可列為分工問題。
- 簡(jiǎn)單并行任務(wù)、聚合任務(wù)和批量并行任務(wù)的現(xiàn)實(shí)的工作流程圖
這三種任務(wù)模型,基本覆蓋日常工作中的并發(fā)場(chǎng)景,但其實(shí)還有一種“分治”任務(wù)模型。
分治,分而治之,一種解決復(fù)雜問題的思維方法和模式。把一個(gè)復(fù)雜問題分解成多個(gè)相似的子問題,然后再把子問題分解成更小的子問題,直到子問題簡(jiǎn)單到可以直接求解。理論上解決每一個(gè)問題都對(duì)應(yīng)著一個(gè)任務(wù),所以對(duì)于問題的分治,實(shí)際上就是對(duì)于任務(wù)的分治。
P8 大佬直接開問,那你說(shuō)說(shuō)什么是分治任務(wù)模型?
分治任務(wù)模型可分為兩個(gè)階段:
- 任務(wù)分解
將任務(wù)迭代地分解為子任務(wù),直至子任務(wù)可計(jì)算出結(jié)果。將地區(qū)具體事務(wù)分屬各個(gè)地方行政官。
- 結(jié)果合并
逐層合并子任務(wù)的執(zhí)行結(jié)果,直至獲得最終結(jié)果。各地方行政官最終將治理成果匯報(bào)上級(jí)。
就像官僚制度一樣:
那你平時(shí)開發(fā)是如何使用Fork/Join的?
我,我平時(shí)還真沒通過(guò)啊,就背過(guò)。還好這道題,我面試前也準(zhǔn)備了…
Fork/Join是一個(gè)并行計(jì)算框架,以支持分治任務(wù)模型
- Fork對(duì)應(yīng)分治任務(wù)模型里的任務(wù)分解
- Join對(duì)應(yīng)結(jié)果合并
Fork/Join計(jì)算框架主要包含兩部分:
- 分治任務(wù)的線程池ForkJoinPool
- 分治任務(wù)ForkJoinTask
這倆的關(guān)系類似于 ThreadPoolExecutor 和 Runnable,都是提交任務(wù)到線程池,只不過(guò)分治任務(wù)有自己獨(dú)特的任務(wù)類型ForkJoinTask。
ForkJoinTask
JDK7 提供,一個(gè)抽象類,核心方法如下:
- fork()
異步執(zhí)行一個(gè)子任務(wù)
- join()
阻塞當(dāng)前線程來(lái)等待子任務(wù)的執(zhí)行結(jié)果
ForkJoinTask有兩個(gè)子類——RecursiveAction和RecursiveTask,顯然都是用遞歸處理分治任務(wù)。這兩個(gè)子類都定義了抽象方法compute():
- RecursiveAction#compute()無(wú)返回值
- RecursiveTask#compute()有返回值
注意到這倆類都是抽象類,使用要定義子類實(shí)現(xiàn)。
只見 P8 開始冷笑,看來(lái)要問源碼級(jí)別原理了!
那你說(shuō)下Fork/Join的工作原理
還好我知道阿里面試套路,凡是 java 工具,必問深入的源碼。
因?yàn)镕ork/Join的核心就是ForkJoinPool,讓我來(lái)深入講解ForkJoinPool原理。
ThreadPoolExecutor本質(zhì)是個(gè)生產(chǎn)者-消費(fèi)者實(shí)現(xiàn),內(nèi)部有一個(gè)任務(wù)隊(duì)列,作為生產(chǎn)者和消費(fèi)者的通信媒介。ThreadPoolExecutor可以有多個(gè)工作線程,這些工作線程都共享一個(gè)任務(wù)隊(duì)列。
ForkJoinPool本質(zhì)上也是一個(gè)生產(chǎn)者-消費(fèi)者的實(shí)現(xiàn),但更智能
- ForkJoinPool工作原理圖
ThreadPoolExecutor內(nèi)部只有一個(gè)任務(wù)隊(duì)列,而ForkJoinPool內(nèi)部有多個(gè)任務(wù)隊(duì)列,當(dāng)調(diào)用ForkJoinPool#invoke()或submit()提交任務(wù)時(shí),F(xiàn)orkJoinPool把任務(wù)通過(guò)路由規(guī)則提交到一個(gè)任務(wù)隊(duì)列,如果任務(wù)在執(zhí)行過(guò)程中會(huì)創(chuàng)建出子任務(wù),那么子任務(wù)會(huì)提交到工作線程對(duì)應(yīng)的任務(wù)隊(duì)列。
如果工作線程對(duì)應(yīng)的任務(wù)隊(duì)列空,是不是就沒活兒干了?
No!ForkJoinPool有個(gè)“任務(wù)竊取”機(jī)制,若工作線程空閑了,它會(huì)“竊取”其他工作任務(wù)隊(duì)列里的任務(wù),例如剛才那個(gè)圖中,線程T2對(duì)應(yīng)任務(wù)隊(duì)列已空
那它會(huì)“竊取”線程T1對(duì)應(yīng)的任務(wù)隊(duì)列的任務(wù)。這樣所有工作線程都不會(huì)閑著。
ForkJoinPool的任務(wù)隊(duì)列采用的是雙端隊(duì)列,工作線程正常獲取任務(wù)和“竊取任務(wù)”分別從任務(wù)隊(duì)列不同的端消費(fèi),這也能避免很多不必要的數(shù)據(jù)競(jìng)爭(zhēng)。
ForkJoinPool支持任務(wù)竊取機(jī)制,能夠讓所有線程的工作量基本公平,不會(huì)出現(xiàn)線程有的很忙,有的一直在摸魚,所以性能很好,是個(gè)很公正的領(lǐng)導(dǎo)。
Java8的Stream API里面并行流也是基于ForkJoinPool。
默認(rèn),所有的并行流計(jì)算都共享一個(gè)ForkJoinPool,這個(gè)共享的ForkJoinPool的默認(rèn)線程數(shù)是CPU核數(shù);
若所有并行流計(jì)算都是CPU密集型,完全沒有問題,但若存在I/O密集型并行流計(jì)算,那很可能因?yàn)橐粋€(gè)很慢的I/O計(jì)算而拖慢整個(gè)系統(tǒng)的性能。所以建議用不同F(xiàn)orkJoinPool執(zhí)行不同類型的計(jì)算任務(wù)。
參考
https://www.liaoxuefeng.com/article/1146802219354112
本文轉(zhuǎn)載自微信公眾號(hào)「JavaEdge」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系JavaEdge公眾號(hào)。