1. 程式人生 > >執行緒複用:執行緒池筆記

執行緒複用:執行緒池筆記

執行緒複用:執行緒池

執行緒池總概

什麼是執行緒池?

接觸過JDBC的人,一定聽說過資料庫連線池(比如,c3p0、Druid等)。其實在我的理解中,兩者是差不多的。不過執行緒池中放的是執行緒而已。
執行緒是一種輕量級工具,但其建立與關閉都需要花費一定的時間。而且大量的執行緒會搶佔記憶體資源。盲目的大量資源會對系統造成極大的壓力。
執行緒池,中有一定數量的活躍執行緒。建立執行緒變成了從執行緒池中獲得空閒執行緒;關閉執行緒變成了向執行緒池歸還執行緒。

JDK對於執行緒池的支援

Java通過Executors提供五種執行緒池,分別為:

  • newCachedThreadPool
    建立一個可快取執行緒池,如果執行緒池長度超過處理需要,可靈活回收空閒執行緒,若無可回收,則新建執行緒。
  • newFixedThreadPool建立一個定長執行緒池,可控制執行緒最大併發數,超出的執行緒會在佇列中等待。
  • newScheduledThreadPool建立一個定長執行緒池,支援定時及週期性任務執行。
  • newSingleThreadExecutor建立一個單執行緒化的執行緒池,它只會用唯一的工作執行緒來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先順序)執行。
  • newSingleThreadScheduledExcutor建立單執行緒化的執行緒池,支援定時及週期性任務執行。

執行緒池的使用

首先是簡單使用,這個沒有什麼特殊之處。
只需記得newFixedThreadPool建立的是定長的執行緒池,可控制執行緒最大併發數,超出的執行緒會在佇列中等待。
newCachedThreadPool建立的執行緒池為無限大,當執行第二個任務時第一個任務已經完成,會複用執行第一個任務的執行緒,而不用每次新建執行緒

定時任務

newScheduledThreadPool支援定時及週期性任務執行,查看了其原始碼,主要有以下三種方法:

  • schedule():在給定時間,對任務進行排程;
  • scheduleAtFixedRate() 和 scheduleWithFixedDelay():對任務進行週期性排程,但兩者有所區別。
scheduleAtFixedRate() 和 scheduleWithFixedDelay() 的區別
  1. 兩種排程的區別:
    • FixedRate 方式:以上一個任務開始執行時間為起點,在之後的延遲時間後,呼叫下一次任務。
    • FixedDelay 方式:上一個任務結束後,再經過延遲時間進行任務排程。
  2. 若任務執行時間超過排程時間,
    • FixedRate 方式:若排程時間過短,那麼任務會在上一個任務結束後立刻呼叫(不會出現任務堆疊的現場)。
    • FixedDelay 方式:會嚴格按照任務間隔時間 = 排程時間 + 任務執行時間

如果任務遇到異常,那麼後續的所有子任務都會停止排程。因此必須保證,異常被及時處理,為週期性任務的穩定排程提供條件。

關於執行緒池的記錄

拒絕策略

建立執行緒池的核心類 ThreadPoolExecutor 有一個引數指定了拒絕策略。拒絕策略,是系統超負荷執行時的補救措施,通常是由於壓力太大而引起的,也就是執行緒池中的執行緒已經用完了且等待佇列已經排滿了。
JDK 提供了四種拒絕策略

  • AbortPolicy 策略:直接丟擲異常,阻止系統正常工作。
  • CallerRnsPolicy 策略:只要執行緒池未關閉,將直接在呼叫者執行緒中執行被丟棄的任務。這種做法不會真的丟棄任務,但是任務提交執行緒的效能將急劇下降
  • DiscardOldestPolicy 策略:丟棄最老的一個請求,也就是即將被執行的任務(處於等待佇列的隊頭),並嘗試再次提交當前任務。
  • DiscardPolicy 策略:直接丟掉無法處理的任務。
  • 自定義策略:自己擴充套件 RejectedExecutionHandler 介面。

執行緒擴充套件

ThreadPoolExecutor是一個可擴充套件的執行緒池,有beforeExecute()afterExecute()terminated()能夠對執行緒進行控制。

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

這是三個protected的空方法,擺明了可以讓子類擴充套件。
* 在執行任務的執行緒中將呼叫beforeExecuteafterExecute等方法,在這些方法中還可以新增日誌、計時、監視或者統計資訊收集的功能。
* 無論是正常執行,還是丟擲異常,都會呼叫afterExecute。但是,如果丟擲Eorror,將不會呼叫該方法;或者beforeExecute丟擲一個RuntimeException,則任務將不被執行,即該方法也不會被呼叫。
* 關於terminated,線上程池完成關閉時(就是在所有任務已經完成且所有工作者執行緒已經關閉),用來釋放Executor在生命週期裡分配的各種資源,此外還能執行資訊通知、日誌記錄等功能。

補充

  1. 使用執行緒池被”吃”掉了異常堆疊資訊
    在使用執行緒池提交執行緒時,可能會發生異常堆疊資訊被”吃”掉的現象,而解決方法:

    • 放棄submit(),改用execute()。
    • 獲取submit()方法返回類的get()方法。

      Future future = pools.submit(new Thread());
      future.get();
    • 擴充套件 ThreadPoolExecutor 執行緒池,讓其在排程任務前,先儲存提交任務執行緒的堆疊訊息(就是重寫執行緒池執行緒的呼叫方法)。

  2. 自定義執行緒:ThreadFactory
    這個介面只有一個方法 newThread(Runnable r),主要是由執行緒池呼叫新建執行緒。

  3. 優化執行緒池執行緒數量
    在《Java Concurrency in Practice》書中給了一個估算執行緒池大小的經驗公式(同時,在Java中,可以通過Runtime.getRuntime().availableProcessors獲取可用的CPU數量。)。

Ncpu = CPU數量
Ucpu = 目標CPU的使用率,0 <= Ucpu <= 1
W/C = 等待時間與計算時間的比率
所以,最優的執行緒池大小為:
Nthreads = Ncpu * Ucpu * ( 1 + W/C )

參考資料

  • 《實戰Java高併發程式設計》(葛一鳴 郭超 著)