回覆列表
  • 1 # 多維的世界

    執行緒池是為了解決執行緒建立資源消耗問題而出現的。所以要更好的使用執行緒池就要分別從執行緒池大小引數的設定、工作執行緒的建立、空閒執行緒的回收、阻塞佇列的使用、任務拒絕策略、執行緒池Hook等方面來了解執行緒池的使用,其中涉及到一些細節包括不同引數、不同佇列、不同拒絕策略的選擇、產生的影響和行為、為更好的使用執行緒池奠定知識基礎。

    ExecutorService基於池化的執行緒來執行使用者提交的任務,通常可以簡單的透過Executors提供的工廠方法來建立ThreadPoolExecutor例項。

    執行緒池解決的兩個問題:

    執行緒池透過減少每次做任務的時候產生的效能消耗來最佳化執行大量的非同步任務的時候的系統性能。執行緒池還提供了限制和管理批次任務被執行的時候消耗的資源、執行緒的方法。另外ThreadPoolExecutor還提供了簡單的統計功能,比如當前有多少任務被執行完了。

    快速開始

    為了使得執行緒池適合大量不同的應用上下文環境,ThreadPoolExecutor提供了很多可以配置的引數和可被用來擴充套件的鉤子。然而,使用者還可以透過使用Executors提供的一些工廠方法來快速建立ThreadPoolExecutor例項。比如:

    使用Executors#newCachedThreadPool可以快速建立一個擁有自動回收執行緒功能且沒有限制的執行緒池。使用Executors#newFixedThreadPool可以用來建立一個固定執行緒大小的執行緒池。使用Executors#newSingleThreadExecutor可以用來建立一個單執行緒的執行器。

    如果上面的方法建立的例項不能滿足我們的需求,我們可以自己透過引數來配置,例項化一個例項。

    關於執行緒數大小引數設定

    ThreadPoolExecutor會根據corePoolSize和maximumPoolSize來動態調整執行緒池的大小:poolSize。

    當任務透過executor提交給執行緒池的時候,我們需要知道下面幾個點:

    如果這個時候當前池子中的工作執行緒數小於corePoolSize,則新建立一個新的工作執行緒來執行這個任務,不管工作執行緒集合中有沒有執行緒是處於空閒狀態。如果池子中有比corePoolSize大的但是比maximumPoolSize小的工作執行緒,任務會首先被嘗試著放入佇列,這裡有兩種情況需要單獨說一下:如果任務被成功的放入佇列,則看看是否需要開啟新的執行緒來執行任務,只有噹噹前工作執行緒數為0的時候才會建立新的執行緒,因為之前的執行緒有可能因為都處於空閒狀態或因為工作結束而被移除。如果放入佇列失敗,則才會去建立新的工作執行緒。如果corePoolSize和maximumPoolSize相同,則執行緒池的大小是固定的。透過將maximumPoolSize設定為無限大,我們可以得到一個無上限的執行緒池。除了透過構造引數設定這幾個執行緒池引數之外我們還可以在執行時設定。

    核心執行緒WarmUp

    預設情況下,核心工作執行緒值在初始的時候被建立,當新任務來到的時候被啟動,但是我們可以透過重寫prestartCoreThread或prestartCoreThreads方法來改變這種行為。通常場景我們可以在應用啟動的時候來WarmUp核心執行緒,從而達到任務過來能夠立馬執行的結果,使得初始任務處理的時間得到一定最佳化。

    定製工作執行緒的建立

    新的執行緒是透過ThreadFactory來建立的,如果沒有指定,預設的Executors#defaultThreadFactory將被使用,這個時候建立的執行緒將都屬於同一個執行緒組,擁有同樣的優先順序和daemon狀態。擴充套件配置ThreadFactory,我們可以配置執行緒的名字、執行緒組合daemon狀態。如果呼叫ThreadFactory#createThread的時候失敗,將返回null,executor將不會執行任何任務。

    空閒執行緒回收

    如果當前池子中的工作執行緒數大於corePoolSize,如果超過這個數字的執行緒處於空閒的時間大於keepAliveTime,則這些執行緒將會被終止,這是一種減少不必要資源消耗的策略。這個引數可以在執行時被改變,我們同樣可以將這種策略應用給核心執行緒,我們可以透過呼叫allowCoreThreadTimeout來實現。

    選擇合適的阻塞佇列

    所有的阻塞佇列都可以被用來存放任務,但是使用不同的佇列針對corePoolSize會表現不同的行為:

    1、當池中工作執行緒數小於corePoolSize的時候,每次來任務的時候都會建立一個新的工作執行緒。

    2、當池中工作執行緒數大於等於corePoolSize的時候,每次任務來的時候都會首先嚐試將執行緒放入佇列,而不是直接去建立執行緒。

    3、如果放入佇列失敗,且當先池中執行緒數小於maximumPoolSize的時候,則會建立一個工作執行緒。

    下面主要是不同佇列策略表現:

    1、直接遞交:一種比較好的預設選擇是使用SynchronousQueue,這種策略會將提交的任務直接傳送給工作執行緒,而不持有。如果當前沒有工作執行緒來處理,即任務放入佇列失敗,則根據執行緒池的實現,會引發新的工作執行緒建立,因此新提交的任務會被處理。這種策略在當提交的一批任務之間有依賴關係的時候避免了鎖競爭消耗。值得一提的是,這種策略最好是配合unbounded執行緒數來使用,從而避免任務被拒絕。同時我們必須要考慮到一種場景,當任務到來的速度大於任務處理的速度,將會引起無限制的執行緒數不斷的增加。

    2、無界佇列:使用無界佇列如LinkedBlockingQueue沒有指定最大容量的時候,將會引起當核心執行緒都在忙的時候,新的任務被放在佇列上,因此,永遠不會有大於corePoolSize的執行緒被建立,因此maximumPoolSize引數將失效。這種策略比較適合所有的任務都不相互依賴,獨立執行。舉個例子,如網頁伺服器中,每個執行緒獨立處理請求。但是當任務處理速度小於任務進入速度的時候會引起佇列的無限膨脹。

    3、有界佇列:有界佇列如ArrayBlockingQueue幫助限制資源的消耗,但是不容易控制。佇列長度和maximumPoolSize這兩個值會相互影響,使用大的佇列和小maximumPoolSize會減少CPU的使用、作業系統資源、上下文切換的消耗,但是會降低吞吐量,如果任務被頻繁的阻塞如IO執行緒,系統其實可以排程更多的執行緒。使用小的佇列通常需要大maximumPoolSize,從而使得CPU更忙一些,但是又會增加降低吞吐量的執行緒排程的消耗。總結一下是IO密集型可以考慮多些執行緒來平衡CPU的使用,CPU密集型可以考慮少些執行緒減少執行緒排程的消耗。

    選擇適合的拒絕策略

    當新的任務到來的而執行緒池被關閉的時候,或執行緒數和佇列已經達到上限的時候,我們需要去做一個決定,怎麼拒絕這些任務。下面介紹一下常用的策略:

    1、ThreadPoolExecutor#AbortPolicy:這個策略直接丟擲RejectedExecutionException異常。

    2、ThreadPoolExecutor#CallerRunsPolicy:這個策略將會使用Caller執行緒來執行這個任務,這是一種feedback策略,可以降低任務提交的速度。

    3、ThreadPoolExecutor#DiscardPolicy:這個策略將會直接丟棄任務。

    4、ThreadPoolExecutor#DiscardOldestPolicy:這個策略將會把任務佇列頭部的任務丟棄,然後重新嘗試執行,如果還是失敗則繼續實施策略。

    除了上面的幾種策略,我們也可以透過實現RejectedExecutionHandler來實現自己的策略。

    利用Hook嵌入你的行為

    ThreadPoolExecutor提供了protected型別可以被覆蓋的鉤子方法,允許使用者在任務執行之前會執行之後做一些事情。我們可以透過它來實現比如初始化ThreadLocal、收集統計資訊、如記錄日誌等操作。這類Hook如beforeExecute和afterExecute。另外還有一個Hook可以用來在任務被執行完的時候讓使用者插入邏輯,如rerminated。

    如果hook方法執行失敗,則內部的工作執行緒的執行將會失敗或被中斷。

    可訪問的佇列

    getQueue方法可以用來訪問queue佇列以進行一些統計或者debug工作,我們不建議用作其他用途。同時remove方法和purge方法可以用來將任務從佇列中移除。

    關閉執行緒池

    當執行緒池不在被引用並且工作執行緒數為0的時候,執行緒池將被終止。我們也可以呼叫shutdown來手動終止執行緒池。如果我們忘記呼叫shutdown,為了讓執行緒資源被釋放,我們還可以使用keepAliveTime和allowCoreThreadTimeOut來達到目的。

  • 中秋節和大豐收的關聯?
  • 把你們手上的搞笑段子都交出來?