這篇是並發編程系列文章第五篇了,說到並發編程,怎么少的了線程池,在阿里線程池使用場景非常多,用好線程池這個利器也算是日常開發必須掌握的了,下面講講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); } }
面試官 : 那這幾種拒絕策略,你選哪一種?
我: 我選拒絕回答
面試官 : 我選你回去等通知。