【面試實戰】# 併發程式設計之執行緒池配置實戰

暴躁的菜鸡發表於2024-06-19

1.先了解執行緒池的幾個引數含義

corePoolSize (核心執行緒池大小):

  • 作用: 指定了執行緒池維護的核心執行緒數量,即使這些執行緒處於空閒狀態,它們也不會被回收。
  • 用途: 核心執行緒用於處理長期的任務,保持最低的執行緒數量,以減少執行緒的建立和銷燬的開銷。

maximumPoolSize (最大執行緒池大小):

  • 作用: 指定了執行緒池中允許的最大執行緒數。超過這個數量的執行緒將不會被建立。
  • 用途: 限制了執行緒池的大小,以防止資源耗盡。

keepAliveTime (執行緒空閒時間):

  • 作用: 當執行緒數超過 corePoolSize 時,多餘的執行緒在空閒時間超過指定時間後將會被終止和回收。
  • 用途: 用於回收不再需要的執行緒,降低資源消耗。只對超過 corePoolSize 的執行緒起作用。

unit (時間單位):

  • 作用: 與 keepAliveTime 一起使用,指定執行緒空閒時間的時間單位(如秒、毫秒)。
  • 用途: 定義 keepAliveTime 的時間單位。

workQueue (任務佇列):

  • 作用: 用於儲存等待執行的任務的佇列。

  • 用途
    管理任務的排隊和處理方式,不同的佇列型別可以影響執行緒池的行為。
    • 常見的佇列型別有:
      • SynchronousQueue: 不儲存任務,任務直接交給執行緒執行。如果沒有空閒執行緒,則建立新執行緒。
      • LinkedBlockingQueue: 無界佇列,可以儲存任意多的任務。只有在任務佇列為空時,才會建立新執行緒。
      • ArrayBlockingQueue: 有界佇列,儲存固定數量的任務,當佇列滿時,任務將被拒絕。

threadFactory (執行緒工廠):

  • 作用: 用於建立執行緒的工廠,可以定製執行緒的建立,比如設定執行緒名、優先順序等。
  • 用途: 統一管理執行緒的建立細節,有助於除錯和監控。

handler (飽和策略/拒絕策略):

  • 作用: 當任務無法提交給執行緒池(例如執行緒池已滿且任務佇列已滿)時,如何處理新任務。

  • 用途
    定義任務無法被執行時的處理方式。
    • 常見策略有:
      • AbortPolicy: 丟擲 RejectedExecutionException 異常。
      • CallerRunsPolicy: 由呼叫者執行緒執行該任務。
      • DiscardPolicy: 丟棄新提交的任務。
      • DiscardOldestPolicy: 丟棄佇列中最舊的任務。

2.調整執行緒池配置應對高併發(常規操作)

為了應對高併發的需求,可以考慮以下調整:

  1. 增大 corePoolSizemaximumPoolSize:
    • 增加核心執行緒和最大執行緒數可以提高執行緒池的併發處理能力,減少任務的等待時間。
  2. 調整 keepAliveTimeunit:
    • 減少 keepAliveTime 可以更快地回收閒置執行緒,釋放資源。相反,增加 keepAliveTime 適用於任務間隔較長的場景,以避免頻繁建立和銷燬執行緒。
  3. 選擇合適的 workQueue:
    • 使用 SynchronousQueue 可以在任務很多但執行緒數不足時迅速增加執行緒數。
    • 使用 LinkedBlockingQueue 可以應對任務佇列過長的問題,但可能導致執行緒數不會增加到最大。
    • 使用 ArrayBlockingQueue 適合在任務數有限的場景,防止資源耗盡。
  4. 合理配置 handler:
    • 根據系統需求選擇適合的拒絕策略。比如,在希望任務儘量被處理時使用 CallerRunsPolicy,在任務不能丟失時選擇 AbortPolicy
  5. 最佳化 threadFactory:
    • 使用自定義的執行緒工廠設定執行緒名、優先順序、守護執行緒等,提高執行緒管理的清晰度和系統穩定性。
  6. 監控和調整:
    • 定期監控執行緒池的效能指標,如任務佇列長度、執行緒使用率等,並根據實際情況動態調整引數配置。
// 建立執行緒池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    10,                   // corePoolSize
    50,                   // maximumPoolSize
    60,                   // keepAliveTime
    TimeUnit.SECONDS,     // keepAliveTime's unit
    new LinkedBlockingQueue<>(100), // workQueue
    Executors.defaultThreadFactory(), // threadFactory
    new ThreadPoolExecutor.AbortPolicy() // handler
);

// 提交任務
executor.submit(() -> {
    // Task implementation
});

// 關閉執行緒池
executor.shutdown();

3.IO密集型、CPU密集型任務的合理配置(生產常用)

3.1 IO密集型任務

IO密集型任務:(例如網路操作、檔案讀寫)通常不需要大量的CPU時間,但可能會等待IO操作的完成。為了有效利用系統資源,可以配置更多的執行緒來掩蓋IO操作的等待時間。

配置建議:

  • corePoolSizemaximumPoolSize:
    • 建議的執行緒數通常遠超過 CPU 核心數,因為執行緒在等待IO操作時不會佔用CPU。可以使用 (CPU 核心數 * 2) 或更多,甚至是 (CPU 核心數 * 2) + 1 這種經驗值。
    • 如果執行緒數太少,CPU資源可能未能充分利用。太多的執行緒可能會導致執行緒上下文切換的開銷。
  • keepAliveTimeunit:
    • 適當地增加 keepAliveTime,讓執行緒在空閒時保留一段時間,以便在短時間內有任務到達時無需重新建立執行緒。
  • workQueue:
    • LinkedBlockingQueue 是常見選擇,因為它可以有效處理大量任務,而不需要頻繁地建立和銷燬執行緒。
    • SynchronousQueue 也可以用於高併發IO場景,確保任務直接交給執行緒執行,迅速響應。

示例:

int numCores = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor ioBoundExecutor = new ThreadPoolExecutor(
    numCores * 2,                // corePoolSize
    numCores * 2 + 1,            // maximumPoolSize
    60L,                         // keepAliveTime
    TimeUnit.SECONDS,            // keepAliveTime's unit
    new LinkedBlockingQueue<>(), // workQueue
    Executors.defaultThreadFactory(), // threadFactory
    new ThreadPoolExecutor.CallerRunsPolicy() // handler
);

3.2 CPU密集型任務

CPU密集型任務:(例如計算密集的操作、資料處理)主要消耗 CPU 資源,因此執行緒數應該與 CPU 核心數相匹配,以避免過度的執行緒上下文切換和資源競爭。

配置建議:

  • corePoolSizemaximumPoolSize:
    • 通常設定為 CPU 核心數CPU 核心數 + 1
    • 過多的執行緒可能導致頻繁的上下文切換,降低效能。
  • keepAliveTimeunit:
    • keepAliveTime 通常設定較短,適合及時回收空閒執行緒。
  • workQueue:
    • SynchronousQueueArrayBlockingQueue 是不錯的選擇,可以避免任務堆積,確保執行緒數控制在合理範圍內。

示例:

int numCores = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor cpuBoundExecutor = new ThreadPoolExecutor(
    numCores,                    // corePoolSize
    numCores + 1,                // maximumPoolSize
    30L,                         // keepAliveTime
    TimeUnit.SECONDS,            // keepAliveTime's unit
    new SynchronousQueue<>(),    // workQueue
    Executors.defaultThreadFactory(), // threadFactory
    new ThreadPoolExecutor.AbortPolicy() // handler
);

3.3 關鍵考慮因素

  1. 系統資源和負載:
    • 監控系統的實際負載和資源使用情況,定期調整配置。
  2. 任務特性:
    • 根據任務的性質(長任務、短任務、IO 密集型、CPU 密集型)選擇合適的執行緒池配置。
  3. 阻塞時間:
    • 對於 IO 密集型任務,理解和分析任務的阻塞時間,並根據其阻塞時間設定合適的執行緒池大小。
  4. 拒絕策略:
    • 合理選擇拒絕策略(如 AbortPolicy, CallerRunsPolicy),確保系統在負載過高時能平穩處理任務。

4.專業級執行緒池配置(大廠規範)

4.1 執行緒池大小的計算公式

IO 密集型任務

對於IO密集型任務,可以使用以下公式計算適合的執行緒池大小:

file

  • N_threads: 推薦的執行緒池大小
  • N_cores: CPU核心數
  • W: 任務的等待時間(包括IO操作的等待時間)
  • C: 任務的計算時間
  • U: 期望的CPU使用率,通常設為0.8~0.9,避免CPU負載過高(0 < U < 1)

解釋: 公式中的 W/C反映了IO操作佔用的時間比,1 - U 是為了預留一定的CPU資源。

示例:

假設有一個任務,CPU核心數為8,IO等待時間為200ms,計算時間為100ms,期望的CPU使用率為80%,則推薦的執行緒池大小為:

file

這意味著你可能需要配置大約120個執行緒來處理IO密集型任務。

CPU 密集型任務

對於CPU密集型任務,執行緒池的大小通常可以透過以下公式估算:

file

在CPU密集型場景下,由於 W 很小或接近於零,因此公式通常簡化為:

file

示例:

假設有一個任務,CPU核心數為8,計算時間大部分佔用時間,等待時間可以忽略不計,則推薦的執行緒池大小為:

file

5.根據TPS和QPS進行執行緒池計算(生產常用)

其實和4的公式差不多

5.1 基礎概念

  • TPS (Transactions Per Second): 每秒系統處理的事務數量。這通常用於描述系統處理更復雜的業務邏輯的能力。
  • QPS (Queries Per Second): 每秒系統處理的查詢數量,通常用於衡量服務端API或資料庫的查詢處理能力。
  • 響應時間: 單個請求或事務的平均處理時間。

5.2 公式:

file

  • N_threads: 推薦的執行緒池大小
  • Q: 每秒的請求數(TPS 或 QPS)
  • R: 平均響應時間(秒)
  • U: 系統期望的CPU利用率(< 1, 通常為80%~90%)

解釋: 公式描述了在滿足特定吞吐量和響應時間的情況下,需要的執行緒數,預留了一部分CPU資源以防過載。

5.3 IO密集型、CPU密集型任務選擇

這裡我們主要舉例說明IO密集型任務

因為:CPU密集型任務主要消耗CPU資源,執行緒數接近CPU核心數就足夠,可以加一個額外的執行緒來處理。Nthreads=Ncores+1

IO密集型:

公式:

file

說明: 由於IO密集型任務在等待IO時不會佔用CPU,因此執行緒數可以較高,適用於處理高併發的IO操作。

示例:

假設系統需要處理每秒500個請求(Q = 500),每個請求的平均響應時間為0.2秒,系統期望的CPU利用率為80%(U = 0.8):

file

這意味著你可能需要大約500個執行緒來處理這些IO密集型請求。

示例程式碼:

int qps = 500;
double responseTime = 0.2;
double targetUtilization = 0.8;

int nThreads = (int) (qps * responseTime / (1 - targetUtilization));

ThreadPoolExecutor ioBoundExecutor = new ThreadPoolExecutor(
    nThreads,                // corePoolSize
    nThreads,                // maximumPoolSize
    60L,                     // keepAliveTime
    TimeUnit.SECONDS,        // keepAliveTime's unit
    new LinkedBlockingQueue<>(), // workQueue
    Executors.defaultThreadFactory(), // threadFactory
    new ThreadPoolExecutor.CallerRunsPolicy() // handler
);

6.總結

  • IO密集型任務: 使用公式 file 計算執行緒池大小。
  • CPU密集型任務: 使用公式 file計算執行緒池大小。
  • 混合型任務: 綜合IO和CPU的公式進行計算和調整。
  • file
    • W: 平均等待時間
    • C: 平均計算時間
  • 實際應用: 根據QPS或TPS、響應時間、期望的CPU利用率等引數進行計算,並定期監控系統負載進行調整。

合理的執行緒池配置可以顯著提升系統的處理能力和資源利用率,因此根據具體需求和系統指標進行精細配置是至關重要的。

相關文章