細嚼慢嚥 Java 線程池,你品你細品

Photo By Instagram natgeoadventure

問題 13

你好同學,我是今天的面試官。咱們來聊聊平時開發中爲什麼要使用線程池技術,Java 線程池它具體是怎麼實現的

好處多多

假設我們不使用線程池技術,那麼就在任務來臨時刻啓動一個新的線程,任務處理結束,釋放線程資源。但是啓動和銷燬線程對服務器來說是比較耗費性能的一件事情,首先當任務來臨時候,由於需要創建新的線程,會造成任務的延遲,其次頻繁的創建和銷燬線程也造成了大量不必要的資源浪費。在使用線程池以後,線程處理完當前任務以後不會被銷燬,當新任務來臨時候會重新利用已經創建好的線程,避免了創建銷燬線程的開銷,同時由於任務來臨時線程已經就緒,也提高了服務的吞吐量。在平時的工作中,有很多地方都使用到了這種池化技術,例如數據庫連接池,網絡請求連接池,在比如說 JDK 中字符串常量池也可以認爲是一種池化技術。

Java 線程池怎麼玩

想玩明白 Java 的線程池,只需要的知道構建線程池的幾個參數具體的含義基本上就明瞭了。那麼接下來,就讓我們一一瓦解這些參數。

corePoolSize

我們假設有 N 個任務需要提交到線程池去處理,當任務數量 N 小於核心線程數 corePoolSize(後文用 C 來代替) 的時候,線程池會不斷新建線程來處理用戶提交進來的任務即使有線程空閒。C 其實代表的是線程池通常情況下會保留的線程數量(如果將線程池比作一個工廠,C 可以類比爲工廠的正式編制人員數量),當任務數量 N 超過核心線程數量 C 的時候,線程池就要用到下一個參數 workQueue 了。

workQueue

當用戶提交的任務數量變多了,這時候線程池中的線程數量已經達到核心線程數 C,那麼只能將提交過來的任務暫存在 workQueue 隊列中。每當有線程處理完手頭上活的時候就會來工作隊列領取任務,如果隊列中沒有任務,那麼當前線程就阻塞在隊列上,等待任務。工作隊列可以簡單分爲 2 無界隊列和有界隊列。

無界隊列

如果我創建線程池的傳入的是無界隊列,那麼意味着用戶可以源源不斷的提交任務到線程池,而不需要擔心線程池拒絕接收,例如 LinkedBlockingQueue 就是一種選擇。

有界隊列

如果我們傳入的是有界隊列,例如 ArrayBlockingQueue,那就需要考慮隊列存滿了怎麼辦?不用擔心這個時候線程池會幫忙找一些臨時工來幹活,這就需要用到下一個參數 maximumPoolSize 了。


maximumPoolSize

此時所有的核心線程都在幹活,而且工作隊列也存滿了任務。如果還是有任務提交進來,那麼線程池會再創建新的線程來幫助工作(可以類比爲一個工廠,管理員發現任務太多,倉庫也堆滿了任務需要僱傭一些臨時工來幫助幹活)。當然臨時工也不能僱傭太多,畢竟工廠資源有限,需要設定工廠裏面工人最大上限,這個就是 maximumPoolSize 了。然而瘋狂的用戶哪管你能不能處理完任務,還是不斷的提交任務進來,這個時候線程池忍無可忍了,關門拒絕用戶提交新的任務,這時候 RejectedExexcutionHandler 就要開始發揮作用了。


RejectedExexcutionHandler

線程池共提供瞭如下 4 種拒絕策略

AbortPolicy  策略會拋出一個 RejectedExecutionException 異常給用戶,告訴它任務被拒絕了。

DiscardPolicy 策略當任務來臨時候不會給用戶任何反饋,悄無聲息拒絕任務。

DiscardOldestPolicy 策略比較霸道,它會直接將最早存儲在工作隊列的任務丟棄掉,然後再試圖去執行當前提交進來的任務。

CallerRunsPolicy 策略呢雖然線程池中的工人不幫忙處理任務了,它會佔用用戶線程去處理當前任務,這也就意味着用戶線程要處理完當前任務纔可以做其他事情。

使用上面的幾個核心參數完美的解決了任務的提交流程和工作分配問題,接下來就要來考慮一下後面的工作了。用戶提交了一大波任務以後,就不在提交了。這時候線程池的中工人都還在呢,如果一直保留這些資源但是又沒有活幹,會造成資源的浪費。這時候就需要用到 keepAliveTime 和 TimeUnit 參數了。

keepAliveTime 和 TimeUnit

這 2 個參數組合起來決定了一個工人最多可以在工廠裏愉快的摸魚時間,如果摸魚時間超過這個限度,這個工人資源就會被釋放,也就是這個空閒線程資源就被回收掉咯。當然啦,線程池會保留核心線程在工廠裏面等待新任務,以備有新任務的到來,我們也可以通過 public void allowCoreThreadTimeOut(boolean value) 方法設置參數,來允許線程池也可以釋放核心線程。


threadFactory

還剩下最後一個參數,它比較簡單,主要用來創建線程,例如我們想讓線程池中的線程做一些定製化的工作就可以自己來定義線程工廠,這樣線程池創建線程的時候就使用我們指定的工廠了。

你可能會覺得構建一個線程還要設置這麼參數,太麻煩了,貼心的 JDK 幫我們在 Executors 中準備了幾個靜態工廠方法,我們一起看一下它們的特性:

newFixedThreadPool(int nThreads) 可以創建一個固定線程數量的線程池,同時它的工作隊列是一個無界隊列。

newSingleThreadExecutor() 可以創建只有一個線程工作的線程池,同時它的工作隊列也是無界隊列。

newCachedThreadPool()  可以創建一個沒上限工作線程的線程池,它使用了 SynchronousQueue 只要有任務過來,如果有空閒的線程,會優先利用空閒的線程池,沒有空閒線程就會新創建線程。

newSingleThreadScheduledExecutor() 創建的是一個具有延遲和循環執行任務線程池,同時它內部也只有一個線程,它的工作隊列是一個具有延遲功能的隊列 DelayedWorkQueue。

newWorkStealingPool() 這種方法是 Java 8 提供的,它實際創建的是一個 ForkJoinTool 而不是 ThreadPoolExecutor 的實例。

如上即爲 5 中創建線程池的工廠方法,大家根據需要選擇適合自己工作的,當然也可以直接使用 ThreadPoolExecutor 來創建一個。

以上即爲昨天的問題的答案,小夥伴們對這個答案是否滿意呢?歡迎留言和我討論。

又要到年末了,你是不是又悄咪咪的開始看機會啦。爲了廣大小夥伴能充足電量,能順利通過 BAT 的面試官無情三連炮,我特意推出大型刷題節目。每天一道題目,第二天給答案,前一天給小夥伴們獨立思考的機會。

點下“在看”,鼓勵一下?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章