Java ThreadPoolExecutor詳解

ThreadPoolExecutor是Java語言對於線程池的實現。池化技術是一種複用資源,減小開銷的技術。線程是操做系統的資源,線程的建立與調度由操做系統負責,線程的建立與調度都要耗費大量的資源,其中線程建立須要佔用必定的內存,而線程的調度須要不斷的切換線程上下文形成必定的開銷。同時線程執行完畢以後就會被操做系統回收,這樣在高併發狀況下就會形成系統頻繁建立線程。java

爲此線程池技術爲了解決上述問題,使線程在使用完畢後不回收而是重複利用。若是線程可以複用,那麼咱們就能夠使用固定數量的線程來解決併發問題,這樣一來不只節約了系統資源,並且也會減小線程上下文切換的開銷。shell

參數

ThreadPoolExecutor的構造函數有7個,它們分別是:併發

  1. corePoolSize(int):線程池的核心線程數量
  2. maximumPoolSize(int):線程池最大線程數量
  3. keepAliveTime(long):保持線程存活的時間
  4. unit(TimeUnit):線程存活時間單位
  5. workQueue(BlockingQueue):工做隊列,用於臨時存放提交的任務
  6. threadFactory(ThreadFactory):線程工廠,用於建立線程
  7. handler(RejectedExecutionHandler):任務拒絕處理器,當線程池沒法再接受新的任務時,會交給它處理

通常狀況下,咱們只使用前五個參數,剩餘兩個咱們使用默認參數便可。函數

任務提交邏輯

其實,線程池建立參數都與線程池的任務提交邏輯密切相關。根據源碼描述能夠得知:當提交一個新任務時(執行線程池的execute方法)會通過三個步驟的處理。高併發

  1. 當任務數量小於corePoolSize時,線程池會建立一個新的線程(建立新線程由傳入參數threadFactory完成)來處理任務,哪怕線程池中有空閒線程,依然會選擇建立新線程來處理
  2. 當任務數量大於corePoolSize時,線程池會將新任務壓入工做隊列(參數中傳遞的workQueue)等待調度。
  3. 當新提交的任務沒法壓入工做隊列時,會檢查當前任務數量是否大於maximumPoolSize。若是小於maximunPoolSize則會新建線程來處理任務(這時咱們的keepAliveTime參數就起做用了,它主要做用於這種狀況下建立的線程,若是任務數量減少,這些線程閒置了,那麼在超過keepAliveTime時間後就會被回收)。若是大於了maximumPoolSize就會交由任務拒絕處理器handler處理。

線程池狀態

正如線程有不一樣的狀態同樣,線程池也擁有不一樣的運行狀態。源碼中提出,線程池有五種狀態,分別爲:測試

  1. RUNNING:運行狀態,不斷接收任務並處理它們。
  2. SHUTDOWN:關閉狀態,不接收新任務,可是會處理工做隊列中排隊的任務。
  3. STOP:中止狀態,不接收新任務,清空工做隊列且不會處理工做隊列的任務。
  4. TIDYING:待終止狀態,此狀態下,任務隊列和線程池都爲空。
  5. TERMINATED:終止狀態,線程池關閉。

如何讓線程不被銷燬

文章開頭說到,線程在執行完畢以後會被操做系統回收銷燬,那麼線程池時如何保障線程不被銷燬?首先看一個測試用例:操作系統

public static void testThreadState()
{
    Thread thread = new Thread(() -> System.out.println("Hello world")); // 建立一個線程
    System.out.println(thread.getState()); // 此時線程的狀態爲NEW
    thread.start(); // 啓動線程,狀態爲RUNNING
    System.out.println(thread.getState());
    try
    {
      thread.join();
      System.out.println(thread.getState()); // 線程運行結束,狀態爲TERMINATED
      thread.start(); // 此時再啓動線程會發生什麼呢?
    } catch (InterruptedException e)
    {
      e.printStackTrace();
    }
}

結果輸出:線程

NEW
RUNNABLE
Hello world
TERMINATED
Exception in thread "main" java.lang.IllegalThreadStateException
	at java.base/java.lang.Thread.start(Thread.java:794)
	at misc.ThreadPoolExecutorTest.testThreadState(ThreadPoolExecutorTest.java:90)
	at misc.ThreadPoolExecutorTest.main(ThreadPoolExecutorTest.java:114)

能夠看出,當一個線程運行結束以後,咱們是不可能讓線程起死回生從新啓動的。既然如此ThreadPoolExecutor如何保障線程執行完一個任務不被銷燬而繼續執行下一個任務呢?code

其實這裏就要講到咱們最開始傳入的參數workQueue,它的接口類型爲BlockingQueue<T>,直譯過來就是阻塞隊列。這中隊列有個特色,就是當隊列爲空而嘗試出隊操做時會阻塞blog

基於阻塞隊列的如上特色,ThreadPoolExecutor採用不斷循環+阻塞隊列的方式來實現線程不被銷燬。

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
      // 從工做隊列中不斷取任務。若是工做隊列爲空,那麼程序會阻塞在這裏
      while (task != null || (task = getTask()) != null) {
        w.lock();
       	// 檢查線程池狀態
        if ((runStateAtLeast(ctl.get(), STOP) ||
             (Thread.interrupted() &&
              runStateAtLeast(ctl.get(), STOP))) &&
            !wt.isInterrupted())
          wt.interrupt();
        try {
          beforeExecute(wt, task);
          try {
            //// 執行任務 ////
            task.run();
            afterExecute(task, null);
          } catch (Throwable ex) {
            afterExecute(task, ex);
            throw ex;
          }
        } finally {
          task = null;
          w.completedTasks++;
          w.unlock();
        }
      }
      completedAbruptly = false;
    } finally {
      processWorkerExit(w, completedAbruptly);
    }
 }

關閉線程池

想要關閉線程池能夠經過調用shutdown()shutdownNow()方法實現。兩種方法有所不一樣,其中調用shutdown()方法會中止接收新的任務,處理工做隊列中的任務,調用這個方法以後線程池會進入SHUTDOWN狀態,此方法無返回值而且不拋出異常。

shutdownNow()方法會中止接收新的任務,並且會返回未完成的任務集合,同時這個方法也會拋出異常。

如何建立一個適應業務背景的線程池

線程池建立有七個參數,這幾個參數的相互做用能夠建立出適應特定業務場景的線程池。其中最爲重要的有三個參數分別爲:corePoolSizemaximumPoolSizeworkQueue。其中前兩個參數已經在上文中做了詳細介紹,而workQueue參數在線程池建立中也極爲重要。workQueue主要有三種:

  1. SynchronousQueue:這個隊列只能容納一個元素,並且只有當隊列爲空時能夠入隊。
  2. ArrayBlockingQueue:這是一個固定容量大小的隊列。
  3. LinkedBlockingQueue:鏈式阻塞隊列,容量無限。

經過上述三種隊列的特性咱們能夠得知,

  1. 當使用SynchronousQueue的時候,老是傾向於新建線程處理請求,若是線程池大小參數設置的很大,那麼線程數量傾向於無限增加。這樣的線程池可以高效處理突發增加的請求,並且處理效率很高,可是開銷很大。
  2. 當使用ArrayBlockingQueue的時候,線程池所能處理的瞬時最大任務量爲隊列大小 + 線程池最大數量,這樣的線程池中規中矩,使用的業務場景不少,具體還需結合業務場景來調配三個參數的大小。例如I/O密集型的場景,多數的線程處於阻塞狀態,爲了提升系統吞吐量,咱們但願可以有多數線程來處理IO。這樣的話咱們偏向於將corePoolSize設置的大一點。並且阻塞隊列大小不要設置很大,同時maximumPoolSize也設置的大一點。
  3. 當使用LinkedBlockingQueue時,線程池的maximumPoolSize參數會失效,由於按照任務提交流程來看,LinkedBlockingQueue能夠無限制地容納任務,天然不會出現隊列沒法工做,新建線程處理的狀況。使用LinkedBlockingQueue能夠平穩地處理一些請求激增的狀況,可是處理效率不會提升,僅僅可以起到必定的緩衝做用。
相關文章
相關標籤/搜索