線程池的使用場景和工作原理


  近期工作中遇到線程池參數配置不當引發的相關問題。基於此,本文主要結合線程池的原理、使用場景、參數配置、使用注意事項等詳細闡述。文章較長,預計閱讀時長 8~10 分鐘,建議收藏。

前置知識

簡單瞭解幾個關鍵詞,我們說說什麼是線程池?

1. 什麼是線程池?

線程池是一種線程管理和複用的機制。

其核心思想是:預先創建一定數量的線程,並把它們保存在線程池中,用的時候拿出來,用完了丟進去。而不是每次有任務過來重新創建新的線程。如下圖,其核心組成:

架構圖

2. 線程池有什麼好處?

線程池的優勢總結爲以下幾點:

  1. 減少資源消耗
  1. 提高系統性能
  1. 提高響應速度
  1. 增強併發性能

線程池有哪些使用場景?

線程池是一種多線程管理工具,它提供了線程的複用和調度功能,可以顯著提高程序的併發性能和資源利用率。以下是線程池的一些常見使用場景:

  1. 文件上傳下載
  1. 異步任務處理
  1. 定時任務
  1. 批處理
  1. 快速響應用戶請求

線程池是如何工作的?

曾經有面試官這樣問:核心線程數 10,最大線程數 20,阻塞隊列最大 100,假如我有 100 個線程同時進來,線程處理任務時間平均按 1s 計算,那理想情況下多長時間可以處理完?

基於上邊的問題,線程池的工作原理可以概括爲以下幾個步驟:

  1. 任務提交
  1. 狀態檢查
  1. 任務封裝
  1. 核心線程處理
  1. 任務隊列處理
  1. 非核心線程處理
  1. 拒絕策略處理
  1. 任務執行與結果獲取

  總之,歸納起來的執行流程:

線程池參數如何設置?

《阿里巴巴 Java 開發手冊》中規約中強調:

因此實際工作中,我們通常自定義線程池。其核心參數:

  public ThreadPoolExecutor(
      int corePoolSize,            // 核心線程數,線程池中始終保持的線程數,即使它們處於空閒狀態
      int maximumPoolSize,          // 最大線程數,線程池中允許的最大線程數
      long keepAliveTime,           // 非核心線程空閒存活時間,當線程池中正在運行的線程數量超過了核心線程數時,多餘的線程在空閒時間達到這個值後會被終止
      TimeUnit unit,                // 存活時間單位,與keepAliveTime一起使用,表示keepAliveTime的時間單位
      BlockingQueue<Runnable> workQueue, // 工作隊列,用於存放待執行任務的阻塞隊列
      ThreadFactory threadFactory,  // 線程工廠,用於創建新線程
      RejectedExecutionHandler handler // 拒絕策略,當任務太多,無法被線程池及時處理時,採取的策略
  )

參數最佳實踐:

實際上,大都數公司的線程池配置依照自身業務場景和機器性能配置的。這裏介紹的只是個參考,能說清楚利弊就好

  1. corePoolSize,  // 核心線程數

如何理解 CPU 密集 和 I/O 密集?

CPU 密集型任務通常在 CPU 上執行進行大量計算(如 RSA 加密) I/O 密集多指 I/O 操作(主要磁盤讀寫,如 數據庫操作等)

附加:如何獲取 CPU 核數?

代碼:

Runtime.getRuntime().availableProcessors();

命令:

$ lscpu
  1. maximumPoolSize          // 最大線程數

具體依據服務器的 I/O 性能,經驗法則:

對於 I/O 密集型應用,通常將 maximumPoolSize 設置爲可用處理器核心數的 5 到 10 倍。

文件處理服務:如果服務需要處理大量的文件上傳和下載,maximumPoolSize 可以設置爲 20 或更多。

  1. keepAliveTime          // 非核心線程空閒存活時間

看業務實時性高不高,一般系統,設置 60s 亦可

  1. unit          // 存活時間單位

看業務實時性高不高,一般系統,設置 s 亦可

  1. workQueue          // 工作隊列

按目前經驗,工作中常用:

LinkedBlockingQueue 是一個無界隊列,適用於任務數量可能突然激增的場景;

ArrayBlockingQueue 是一個有界隊列,適用於需要限制最大任務數量的場景,以避免資源耗盡

  1. threadFactory          // 線程工廠 一般使用默認的也可

  2. handler // 拒絕策略

主要有 4 種拒絕策略:

AbortPolicy:直接丟棄任務,拋出異常,這是默認策略

CallerRunsPolicy:只用調用者所在的線程來處理任務

DiscardOldestPolicy:丟棄等待隊列中最舊的任務,並執行當前任務

DiscardPolicy:直接丟棄任務,也不拋出異常

介於此,給出公司中用到的其中一個案例配置,僅供參考。

@Bean("XXXAsyncPool")
   public Executor myTaskAsyncPool() {
       ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
       //配置核心線程數
       executor.setCorePoolSize(10);
       //配置核心線程數
       executor.setMaxPoolSize(20);
       //配置隊列容量
       executor.setQueueCapacity(1000);
       //設置線程活躍時間
       executor.setKeepAliveSeconds(60);
       //設置線程名
       executor.setThreadNamePrefix("XXXAsyncPool-");
       //設置拒絕策略
       executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
      
       executor.initialize();
       return executor;
   }

拒絕策略先前遇到坑,

篇幅,稍後單獨做個介紹。

參考資料

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/G7djDLyUdbkA0WMm2hnwOA