這篇是併發程式設計系列文章第五篇了,說到併發程式設計,怎麼少的了執行緒池,在阿里執行緒池使用場景非常多,用好執行緒池這個利器也算是日常開發必須掌握的了,下面講講2019年的那一夜,就執行緒池和某位面試官鏖戰了半個小時。
面試官 : 看你簡歷上寫了對系統效能做了優化,能簡單給我介紹一下嗎? 都有哪些優化,你是怎麼衡量優化效果的?
我 : 巴拉巴拉。。。例如我們系統之前要查詢使用者的個人身份資訊、聯絡人資訊、訂單狀態資訊、積分資訊,之前系統是單執行緒序列處理的,我用執行緒池對四個任務並行處理,然後對處理結果合併。
面試官 : 你剛才說用到執行緒池,能跟我講講為什麼用執行緒池嗎? 我建立四個執行緒處理可不可以?
我 : 可以,當然可以。
我 : 但是用執行緒池更合適。阿里巴巴開發規約中有一條:
3.【強制】執行緒資源必須通過執行緒池提供,不允許在應用中自行顯式建立執行緒。
說明:使用執行緒池的好處是減少在建立和銷燬執行緒上所消耗的時間以及系統資源的開銷,解決資源不足的問題。如果不使用執行緒池,有可能造成系統建立大量同類執行緒而導致消耗完記憶體或者“過度切換”的問題。
我 : 就像你去餐廳吃飯,服務員總是提前洗好盤子,不會等你來打飯的時候才洗盤子,盤子就像是執行緒池裡的執行緒,你打飯就是要處理的任務。
面試官 : 那你知道執行緒池的類結構嗎?
我: 這算什麼問題? 不應該是問我核心執行緒數怎麼設定嗎?好吧。。。請看下圖:
- Executor 的定義非常簡單,就定義了執行緒池最本質要做的事,執行任務。
public interface Executor {
void execute(Runnable command);
}
-
ExecutorService 也是個介面,不過他算是把執行緒池的框架搭出來了,告訴要實現它的執行緒池必須提供的一些管理執行緒池的方法。
-
AbstractExecutorService 是普通的執行緒池執行器,ScheduledExecutorService 是定時任務執行緒池。
面試官 : 那你日常開發中是怎麼建立執行緒池的?
我: 我用ThreadPoolExecutor
自定義建立執行緒池。
面試官 : 那你知道執行緒池建立時都有哪些引數嗎?
我: 執行緒池主要的核心引數有7個,我們看 ThreadPoolExecutor
建構函式就知道了
-
corePoolSize :核心執行緒數
-
maximumPoolSize: 最大執行緒數
-
keepAliveTime :執行緒線上程池中不被銷燬的空閒時間,如果執行緒池的執行緒太多,任務比較小,到這個時間就銷燬執行緒池。
unit : keepAliveTime 的時間單位,一般設定成秒或毫秒。
-
workQueue : 任務佇列,存放等待執行的任務
-
threadFactory: 建立執行緒的任務工廠,比如給執行緒命名加上字首,後面會講
-
handler : 拒絕任務處理器,當任務處理不過來時的拒絕處理器
-
allowCoreThreadTimeOut : 是否允許核心執行緒超時銷燬,這個引數不在建構函式中,但重要性也很高
面試官 : 老實說,你是不是來之前背過了,不然怎麼可能都記住了。
我: [掀桌子],不面了,還找什麼工作,要什麼自行車。
我不過是來之前把“安琪拉的部落格”公眾號上的文章都看了個遍。
面試官 : 其實剛才那也是問題,考察面試者是否皮實,我們繼續。。
面試官 : 剛才說了這些核心引數,你能不能跟我講講執行緒池的基本工作原理。
我: 可以的,這裡我給你畫個流程,如下所示:
面試官 : 那按照上面的流程寫段虛擬碼。
我: 還能不能好好面了,讓手撕執行緒池。
那好吧,你對著??的流程圖看,程式碼如下:
面試官 : 不錯,那你平常怎麼管理執行緒池的呢?
我: 我會搞了個執行緒池管理器,比如 ThreadPoolManager,有個私有變數的Map,按照執行緒池的作用給他取個名字,比如起名為: preparePlateThreadPool (準備餐盤執行緒池),把執行緒池名稱定義成常量,和建立好的執行緒池放到管理器的Map裡。
面試官 : 除了你自己用 ThreadPoolExecutor
建立執行緒池,還有別的方式嗎?
我: java.util.concurrent
包裡提供的 Executors
也可以用來建立執行緒池。
面試官 : Executors
定義了哪幾種 ?
我:
- newSingleThreadExecutos 單執行緒執行緒池,也就是執行緒池只有一個任務,這個我偶爾用一用
- newFixedThreadPool(int nThreads) 固定大小執行緒的執行緒池
- newCachedThreadPool() 無界執行緒池,這個就是無論多少任務,都建立執行緒來執行,所以佇列相當於沒用。
面試官 : 你上面講日常開發自己 用 ThreadPoolExecutor
建立執行緒池,為什麼不用Executors
提供的。
我: 第一是 Executors
提供的執行緒池使用場景很有限,一般場景很難用到,第二他們也都是通過 ThreadPoolExecutor
建立的執行緒池,我直接用 ThreadPoolExecutor
建立執行緒池,可以理解原理,靈活度更高。
參考阿里開發手冊規約:
4.【強制】執行緒池不允許使用Executors去建立,而是通過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同學更加明確執行緒池的執行規則,規避資源耗盡的風險。
說明:Executors返回的執行緒池物件的弊端如下:
1)FixedThreadPool
和SingleThreadPool
:
允許的請求佇列長度為Integer.MAX_VALUE,可能會堆積大量的請求,從而導致OOM。
2)CachedThreadPool
:
允許的建立執行緒數量為Integer.MAX_VALUE,可能會建立大量的執行緒,從而導致OOM。《阿里巴巴研發手冊》
面試官 : 前面你程式碼裡有任務入隊的操作,你一般自定義執行緒池,用的什麼佇列?
我: 這個要看實際應用的。
- 有的任務在早上8點和晚上6點都是高峰期,因此有任務尖刺,用
LinkedBlockingQueue
, 這個是無界佇列,不限制任務大小的。 - 對於重要性沒那麼高,非強依賴的任務用的
ArrayBlockingQueue
,這個是指定大小的,如果任務超出,會建立非核心執行緒執行任務。
面試官 : 那你怎麼保證任務佇列的可用性呢?
我: 分幾個方面:
- 我的執行緒池管理器,會有一個定時任務,定時檢測Map 中執行緒池當前任務佇列的狀態,會設定一個 waterThreshold(水位線),超出水位線會有告警;
- 日常大促演練,會對執行緒池做壓測,如果發生超水位情況,還會對執行緒按執行緒名做降級,動態調整核心執行緒數和佇列,當然還有限流、降級等其他有段保障。
面試官 : 那你怎麼合理拆分執行緒池,核心任務數和任務佇列大小的呢?
我: 這個是個老生常談的問題。
【推薦】 瞭解每個服務大致的平均耗時,可以通過獨立執行緒池配置,將較慢的服務與主執行緒池隔離開,不致於各服務執行緒同歸於盡。
《阿里巴巴研發手冊》
-
按照任務的型別,對任務做拆分,分成不同的執行緒池,分別命名;
-
區分任務的型別,是CPU密集型還是IO密集型,CPU 可以設定約為CPU核心數,上下文切換少,io密集型可以設定的大一些。
-
大體估算一個,然後做壓測,評估,另外執行緒池有個變數也可以參考意義: largestPoolSize,執行緒池達到過的最大執行緒任務,比如你剛開始可以把執行緒數設定的足夠大,壓測過後看這個引數達到的最大數值,同時參考系統的效能指標,cou、io、mem等。
-
這裡還有個公式借鑑: 最佳執行緒數目 = ((執行緒等待時間+執行緒CPU時間)/執行緒CPU時間 )* CPU數目
-
也有開源的輔助測算執行緒池的合理執行緒數。
面試官 : 那拒絕策略呢? 瞭解嗎
我: 拒絕策略就是當任務太多,超過maximumPoolSize了,只能拒絕。
面試官 : 詳細講講
我: 拒絕的時候可以指定拒絕策略,也可以自己實現,JDK預設提供了四種拒絕策略.
-
AbortPolicy
預設拒絕策略, 直接拋RejectedExecutionException
-
DiscardPolicy
任務直接丟棄,不丟擲異常
-
CallerRunsPolicy
由呼叫者來執行被拒絕的任務,比如主執行緒呼叫執行緒池的submit提交任務,但是任務被拒絕,則主執行緒直接執行。
但是執行緒池如果已經被關閉了,任務就被丟棄了。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { //執行緒池沒關閉 if (!e.isShutdown()) { //直接run,沒有讓執行緒池來執行 r.run(); } }
-
DiscardOldestPolicy
丟棄佇列裡等的最久的任務,然後嘗試執行被拒絕的任務。
但是執行緒池如果已經被關閉了,任務就被丟棄了
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { //丟棄佇列頭部任務 e.getQueue().poll(); //執行緒池嘗試執行任務 e.execute(r); } }
面試官 : 那這幾種拒絕策略,你選哪一種?
我: 我選拒絕回答
面試官 : 我選你回去等通知。