執行緒池7個引數拿捏死死的,完爆面試官

語言: CN / TW / HK

執行緒池

  • 上一章節我們介紹的四種建立執行緒的方式算是熱身運動了。執行緒池才是我們的重點介紹物件。

image-20211214192828938.png

  • 這個是JDK對執行緒池的介紹。

image-20211214193012582.png

  • 但是你會問為什麼上面我們建立執行緒池的方式是通過Executors.newCachedThreadPool();

image-20211214193421625.png

image-20211214193626746.png

  • 關於Exectors內部提供了很多快捷建立執行緒的方式。這些方法內部都是依賴ThreadPoolExecutor。所以執行緒池的學習就是ThreadPoolExecutor

  • 執行緒池ThreadPoolExecutor正常情況下最好用執行緒工廠來建立執行緒。他的作用是用來處理每一次提交過來的任務;ThreadPoolExecutor可以解決兩個問題

    • 在很大併發量的情況下執行緒池不僅可以提供穩定的處理還可以減少執行緒之間的排程開銷。
    • 並且執行緒池提供了對執行緒和資源的回收及管理。
  • 另外在內部ThreadPoolExecutor提供了很多引數及可擴充套件的地方。同時他也內建了很多工廠執行器方法供我們快速使用,比如說Executors.newCacheThreadPool():無限制處理任務。 還有Executors.newFixedThreadPool():固定執行緒數量;這些內建的執行緒工廠基本上能滿足我們日常的需求。如果內建的不滿足我們還可以針對內部的屬性進行個性化設定

image-20211215135754716.png

  • 通過跟蹤原始碼我們不難發現,內建的執行緒池構建都是基於上面提到的7個引數進行設定的。下面我畫了一張圖來解釋這7個引數的作用。

image-20211215141012974.png

  • 上面這張圖可以解釋corePoolSizemaxiumPoolSizekeepAliveTimeTimeUnitworkQueue 這五個引數。關於threadFactoryhandler是形容過程中的兩個引數。
  • 關於ThreadPoolExecutor我們還得知道他雖然是執行緒池但是也並不是一開始就初始化好執行緒的。而是根據任務實際需求中不斷的構建符合自身的執行緒池。那麼構建執行緒依賴誰呢?上面也提到了官方推薦使用執行緒工廠。就是我們這裡的ThreadFactory類。
  • 比如Executors.newFixedThreadPool是設定了固定的執行緒數量。那麼當任務超過執行緒數和佇列長度總和時,該怎麼辦?如果真的發生那種情況我們只能拒絕提供執行緒給任務使用。那麼該如何拒絕這裡就涉及到我們的RejectExecutionHandler
  • 點進原始碼我們可以看到預設的佇列好像是LinkedBlockingQueue ; 這個佇列是連結串列結構的怎麼會有長度呢? 的確是但是Executors還給我們提供了很多擴充套件性。如果我們自定義的話我們能夠發現還有其他的

image-20211215142858176.png

核心數與匯流排程數

  • 這裡對應corePoolSizemaxiumPoolSize

final ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);          executorService.execute(new Runnable() {              @Override              public void run() {                  System.out.println("我是執行緒1做的事情");             }         });

  • 我們已newFixedThreadPool來分析下。首先它需要一個整數型引數。

public static ExecutorService newFixedThreadPool(int nThreads) {          return new ThreadPoolExecutor(nThreads, nThreads,                                        0L, TimeUnit.MILLISECONDS,                                        new LinkedBlockingQueue<Runnable>());     }

  • 而實際上內部是構建一個最大執行緒數量為10,且10個執行緒都是核心執行緒(公司核心員工);這10個執行緒是不會有過期時間一說的。過期時間針對非核心執行緒存活時間(公司外包員工)。
  • 當我們執行execute方法時。點進去看看我們發現

image-20211215145232205.png

  • 首先會判斷當前任務數是否超過核心執行緒數,如果沒有超過則會新增值核心執行緒佇列中。注意這裡並沒有去獲取是否有空閒執行緒。而是隻要滿足小於核心執行緒數,進來的任務都會優先分配執行緒。

image-20211215145509226.png

  • 但是當任務數處於(corePoolSize,maxiumPoolSize】之間時,執行緒池並沒有立馬建立非核心執行緒,這點我們從原始碼中可以求證。

image-20211215145810633.png

  • 這段程式碼時上面if 判斷小於核心執行緒之後的if , 也就是如果任務數大於核心執行緒數。優先執行該if 分支。意思就是會將核心執行緒來不及處理的放在佇列中,等待核心執行緒緩過來執行。像我們上面所說如果這個時候我們用的時有邊界的佇列的話,那麼佇列總有放滿的時候。這個時候執行來到我們第三個if分支

image-20211215150107158.png

  • 這裡還是先將任務新增到非核心佇列中。false表示非核心。如果能新增進去說明還沒有溢位非核心數。如果溢位了正好if新增就是false . 就會執行了拒絕策略。
  • 下面時executor執行原始碼

int c = ctl.get();          if (workerCountOf(c) < corePoolSize) {              if (addWorker(command, true))                  return;              c = ctl.get();         }          if (isRunning(c) && workQueue.offer(command)) {              int recheck = ctl.get();              if (! isRunning(recheck) && remove(command))                  reject(command);              else if (workerCountOf(recheck) == 0)                  addWorker(null, false);         }          else if (!addWorker(command, false))              reject(command);     }

思考

  • 基於上面我們對核心數和總數的講述,我們來看看下面這段程式碼是否能夠正確執行吧。

public static void main(String[] args) throws InterruptedException {          ThreadPoolExecutor executorService = new ThreadPoolExecutor(10,20,0,TimeUnit.SECONDS,new ArrayBlockingQueue<>(10));          for (int i = 0; i < 100; i++) {              int finalI = i;              executorService.execute(new Runnable() {                  @SneakyThrows                  @Override                  public void run() {                      System.out.println(finalI);                      TimeUnit.SECONDS.sleep(1000);                 }             });         }     }

image-20211215150903146.png

  • 很不幸,我們的執行報錯了。而且出發了ThreadPoolExecutor中的拒絕策略。而且分析日誌我們能夠發現成功執行的有20個任務。分別是【0,9】+【20,29】這20個任務。
  • 拒絕我們很容易理解。因為我們設定了最大20個執行緒數加上長度為10的佇列。所以該執行緒城同時最多隻能支援30個任務的併發。另外因為我們每一個任務執行時間至少在1000秒以上,所以程式執行到第31個的時候其他都沒有釋放執行緒。沒有空閒的執行緒給第31個任務所以直接拒絕了。
  • 那麼為什麼是是【0,9】+【20,29】呢?上面原始碼分析我們也提到了,進來的任務優先分配核心執行緒數,然後是快取到佇列中。當佇列滿了之後才會分配非核心數。當第31個來臨直接出發拒絕策略,所以不管是核心執行緒還是非核心執行緒都沒有時間處理佇列中的10個執行緒。所以列印是跳著的。
「其他文章」