天天看點

萬字總結最全Java線程池ThreadPoolExecutor面試題(八)總結

關閉線程池

可調用線程池的shutdown或shutdownNow方法關閉線程池。

它們都是周遊線程池中的工作線程,然後逐個調用線程的**interrupt()**來中斷線程,是以無法響應中斷的任務可能永遠無法終止。

  • shutdownNow

    首先将線程池的狀态設成STOP,然後嘗試停止所有正在執行或暫停任務的線程,并傳回等待執行任務的清單

  • shutdown

    隻是将線程池的狀态設成SHUTDOWN态,然後中斷所有沒有正在執行任務的線程

隻要調用了這兩個關閉方法中的任一,isShutdown() 就會傳回true。當所有任務都已關閉後,才表示線程池關閉成功,這時調用isTerminaed方法會傳回true。

至于應該調用哪一種方法,應該由送出到線程池的任務的特性決定:

  • 通常調用shutdown() 關閉線程池
  • 若任務不一定要執行完,則可以調用shutdownNow

合理配置

要想合理地配置線程池,就必須首先

分析任務特性

可從以下幾個角度來分析

  • 任務的性質:CPU密集型任務、IO密集型任務和混合型任務
  • 任務的優先級:高、中和低
  • 任務的執行時間:長、中和短
  • 任務的依賴性:是否依賴其他系統資源,如資料庫連接配接。

性質不同的任務

可以用不同規模的線程池分開處理

CPU密集型任務(計算型任務)

吞吐量較大的計算型任務,線程數量應該較少,可為

N(CPU核數)+1

或 

N(CPU核數) * 2

,因為此時線程一定排程到某個CPU執行,若任務本身是CPU綁定的任務,那麼過多的線程隻會增加線程切換的開銷,而不能提升吞吐量,但可能需要較長隊列做緩沖。

I/O密集型任務

執行較慢、數量不大的IO任務,要考慮更

多線程

數,而無需太大隊列。

相比計算型任務,需多一些線程,要結合具體的 I/O 阻塞時長考慮

如Tomcat中預設的最大線程數為: 200。

也可考慮根據需要在一個最小數量和最大數量間彈性地自動增減線程數。

業務讀取較多,線程并不是一直在執行任務,則應配置盡可能多的線程

N(CPU)/1 - 阻塞系數(0.8~0.9)

一般生産環境下的CPU使用率達到80,說明充分利用了。

混合型的任務

如果可以拆分,将其拆分成一個CPU密集型任務和一個IO密集型任務,隻要這兩個任務執行的時間相差不是太大,那麼分解後執行的吞吐量将高于串行執行的吞吐量.如果這兩個任務執行時間相差太大,則沒必要進行分解.

可以通過Runtime.getRuntime().availableProcessors()方法獲得目前裝置的CPU個數.

優先級不同的任務可以使用PriorityBlockingQueue處理.它可以讓優先級高

的任務先執行.

注意 如果一直有優先級高的任務送出到隊列裡,那麼優先級低的任務可能永遠不能執行

執行時間不同的任務可以交給不同規模的線程池來處理,或者可以使用優先級隊列,讓執行時間短的任務先執行.

依賴資料庫連接配接池的任務,因為線程送出SQL後需要等待資料庫傳回結果,等待的時間越長,則CPU空閑時間就越長,那麼線程數應該設定得越大,這樣才能更好地利用CPU.

建議使用有界隊列 有界隊列能增加系統的穩定性和預警能力,可以根據需要設大一點,比如幾千.

假如系統裡背景任務線程池的隊列和線程池全滿了,不斷抛出抛棄任務的異常,通過排查發現是資料庫出現了問題,導緻執行SQL變得非常緩慢,因為背景任務線程池裡的任務全是需要向資料庫查詢和插入資料的,是以導緻線程池裡的工作線程全部阻塞,任務積壓線上程池裡.

如果我們設定成無界隊列,那麼線程池的隊列就會越來越多,有可能會撐滿記憶體,導緻整個系統不可用,而不隻是背景任務出現問題.

複用線程池不代表應用程式始終使用同一個線程池,我們應該根據任務的性質來選用不同的線程池。特别注意IO綁定的任務和CPU綁定的任務對于線程池屬性的偏好,如果希望減少任務間的互相幹擾,考慮按需使用隔離的線程池。

2.5 線程池的監控

如果在系統中大量使用線程池,則有必要對線程池進行監控,友善在出現問題時,可以根據線程池的使用狀況快速定位問題.可通過線程池提供的參數進行監控,在監控線程池的時候可以使用以下屬性:

  • taskCount:線程池需要執行的任務數量
  • completedTaskCount:線程池在運作過程中已完成的任務數量,小于或等于taskCount。
  • largestPoolSize:線程池裡曾經建立過的最大線程數量.通過這個資料可以知道線程池是否曾經滿過.如該數值等于線程池的最大大小,則表示線程池曾經滿過.
  • getPoolSize:線程池的線程數量.如果線程池不銷毀的話,線程池裡的線程不會自動銷毀,是以這個大小隻增不減.
  • getActiveCount:擷取活動的線程數.

通過擴充線程池進行監控.可以通過繼承線程池來自定義線程池,重寫線程池的

beforeExecute、afterExecute和terminated方法,也可以在任務執行前、執行後和線程池關閉前執行一些代碼來進行監控.例如,監控任務的平均執行時間、最大執行時間和最小執行時間等.

這幾個方法線上程池裡是空方法.

/

protected void beforeExecute(Thread t, Runnable r) { }
      

線程池作為應用程式内部的核心元件往往缺乏監控,往往到程式崩潰後才發現線程池的問題,很被動。

2.6 線程池的狀态

1.當線程池建立後,初始為 running 狀态

2.調用 shutdown 方法後,處 shutdown 狀态,此時不再接受新的任務,等待已有的任務執行完畢

3.調用 shutdownnow 方法後,進入 stop 狀态,不再接受新的任務,并且會嘗試終止正在執行的任務。

4.當處于 shotdown 或 stop 狀态,并且所有工作線程已經銷毀,任務緩存隊列已清空,線程池被設為 terminated 狀态。

總結

線程池有哪些關鍵屬性?

workQueue 用于存放任務,添加任務的時候,如果目前線程數超過了 corePoolSize,那麼往該隊列中插入任務,線程池中的線程會負責到隊列中拉取任務。

keepAliveTime 用于設定空閑時間,如果線程數超出了 corePoolSize,并且有些線程的空閑時間超過了這個值,會執行關閉這些線程的操作

rejectedExecutionHandler 用于處理當線程池不能執行此任務時的情況,預設有抛出 RejectedExecutionException 異常、忽略任務、使用送出任務的線程來執行此任務和将隊列中等待最久的任務删除,然後送出此任務這四種政策,預設為抛出異常。

線程池中的線程建立時機?

如果目前線程數少于 corePoolSize,那麼送出任務的時候建立一個新的線程,并由這個線程執行這個任務;

如果目前線程數已經達到 corePoolSize,那麼将送出的任務添加到隊列中,等待線程池中的線程去隊列中取任務;

如果隊列已滿,那麼建立新的線程來執行任務,需要保證池中的線程數不會超過 maximumPoolSize,如果此時線程數超過了 maximumPoolSize,那麼執行拒絕政策。

任務執行時發生異常怎麼辦?

若某任務執行出現異常,則執行任務的線程會被關閉,而不是繼續接收其他任務。然後會啟動一個新的線程來代替它。

何時執行拒絕政策?

workers 的數量達到了 corePoolSize,任務入隊成功,以此同時線程池被關閉了,而且關閉線程池并沒有将這個任務出隊,那麼執行拒絕政策。這裡說的是非常邊界的問題,入隊和關閉線程池并發執行,讀者仔細看看 execute 方法是怎麼進到第一個 reject(command) 裡面的。

  • workers 的數量大于等于 corePoolSize,準備入隊,可是隊列滿了,任務入隊失敗,那麼準備開啟新的線程,可是線程數已經達到 maximumPoolSize,那麼執行拒絕政策

線程池過多造成OOM 因為活躍線程過多和線程池不會被回收

Java Stream Api異步分流 公用一個預設forkjion線程池,使用時要注意

線程池建立時要分析執行任務是IO資源型還是CPU資源型

IO資源型或者說執行較長時間任務,并且拒絕政策為Call時,會線上程池滿狀态後交給調用者線程執行,如果是Web服務跑在tomcat,就導緻整體吞吐量下降

參考