【連載 15】執行緒池選擇

FunTester發表於2025-02-06

3.2 執行緒池選擇

執行緒池作為壓測引擎的核心執行器,是構建整個方案的重中之重。第 1 章我們已經講過了執行緒池的常見型別以及適用場景,這裡不多贅述。因為我們選擇的是執行緒模型,為了更好的管理執行緒及任務,我們選擇自定義執行緒池。設計執行緒池引數考慮以下幾點:

  • 保障足夠執行緒資源執行測試用例。
  • 保障測試任務提交後快速被執行。
  • 保障執行緒複用,避免測試過程中頻繁建立和銷燬執行緒。
  • 保障不會建立遠超需求的執行緒。

這些條件對應到執行緒池引數上,可以參考 java.util.concurrent.Executors#newFixedThreadPool(int) 方法的內容:

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

這裡做幾點修改:

  • 將執行緒 keepAliveTime 調整為 60 秒,這樣可以提升執行緒複用機會。
  • 我們增加 threadFactory 引數,方便我們列印日誌和排查故障。
  • workQueue 我們限制最大長度,設定為 1,或者使用 java.util.concurrent.SynchronousQueue 代替 java.util.concurrent.LinkedBlockingQueue

演示程式碼如下:

package org.funtester.performance.books.chapter03.section2;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * ThreadFactory 演示類
 */
public class ThreadFactoryDemo {

    public static void main(String[] args) {
        ThreadFactory threadFactory = new ThreadFactory() {// 建立一個執行緒工廠

            AtomicInteger index = new AtomicInteger();// 執行緒安全的執行緒編號

            @Override
            public Thread newThread(Runnable r) {// 重寫建立執行緒方法
                Thread thread = new Thread(r);// 建立執行緒
                thread.setName("執行緒-" + index.incrementAndGet());// 設定執行緒名稱
                return thread;// 返回建立的執行緒
            }
        };
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 3, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10), threadFactory);// 建立執行緒池
        for (int i = 0; i < 8; i++) {// 向執行緒池提交8個任務
            threadPoolExecutor.execute(new Runnable() {// 提交任務
                @Override
                public void run() {// 任務執行邏輯
                    System.out.println(System.currentTimeMillis() + "  執行緒池中的執行緒名稱: " + Thread.currentThread().getName());// 輸出執行緒名稱
                    try {
                        Thread.sleep(100);// 模擬任務執行時間
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);// 丟擲執行時異常
                    }
                }
            });
        }
        threadPoolExecutor.shutdown();// 關閉執行緒池
    }
}

控制檯輸出:

1699978852559  執行緒池中的執行緒名稱: 執行緒-1
1699978852559  執行緒池中的執行緒名稱: 執行緒-2
1699978852559  執行緒池中的執行緒名稱: 執行緒-3
1699978852663  執行緒池中的執行緒名稱: 執行緒-3
1699978852664  執行緒池中的執行緒名稱: 執行緒-2
1699978852664  執行緒池中的執行緒名稱: 執行緒-1
1699978852768  執行緒池中的執行緒名稱: 執行緒-3
1699978852769  執行緒池中的執行緒名稱: 執行緒-2

為了讓每一個執行緒的名字都不一樣,筆者在 ThreadFactory 實現中增加了 AtomicInteger 物件,用於對建立執行緒進行執行緒安全的計數。我們建立了最大執行緒數為 3 的執行緒池,然後利用執行緒工廠設定了每個執行緒的名字,且具有唯一性。為了展示執行緒複用效果,增加了 workQueue 的容量,避擴音交任務時被拒絕。根據列印資訊可以得出實際效果符合預期的結論。

在使用的過程中,如果執行緒在執行任務遭遇中斷情況,會重新建立新的執行緒補充到執行緒池中,相應的執行緒工廠中的計數器就會增加,大於執行緒池最大執行緒數。

以上執行緒池的選擇基於執行緒模型,若是選擇 TPS 模型,一般我們會盡量選擇最大執行緒數遠大於核心執行緒數,workQueue 容量非常小或者使用 java.util.concurrent.SynchronousQueue,下面是一個簡單的例子。

package org.funtester.performance.books.chapter03.section2;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class TheadPoolForTpsModel {

    public static void main(String[] args) {
        ThreadFactory threadFactory = new ThreadFactory() {// 建立一個執行緒工廠

            AtomicInteger index = new AtomicInteger();// 執行緒安全的執行緒編號

            @Override
            public Thread newThread(Runnable r) {// 重寫建立執行緒方法
                Thread thread = new Thread(r);// 建立執行緒
                thread.setName("執行緒-" + index.incrementAndGet());// 設定執行緒名稱
                return thread;// 返回建立的執行緒
            }
        };
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 200, 60L, TimeUnit.SECONDS, new SynchronousQueue<>(), threadFactory);// 建立執行緒池

        for (int i = 0; i < 8; i++) {// 向執行緒池提交8個任務
            threadPoolExecutor.execute(new Runnable() {// 提交任務
                @Override
                public void run() {// 任務執行邏輯
                    System.out.println(System.currentTimeMillis() + "  執行緒池中的執行緒名稱: " + Thread.currentThread().getName());// 輸出執行緒名稱
                    try {
                        Thread.sleep(100);// 模擬任務執行時間
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);// 丟擲執行時異常
                    }
                }
            });
        }
        threadPoolExecutor.shutdown();// 關閉執行緒池
    }
}

控制檯輸出:

1700012277892  執行緒池中的執行緒名稱: 執行緒-1
1700012277893  執行緒池中的執行緒名稱: 執行緒-4
1700012277892  執行緒池中的執行緒名稱: 執行緒-3
1700012277892  執行緒池中的執行緒名稱: 執行緒-2
1700012277893  執行緒池中的執行緒名稱: 執行緒-6
1700012277893  執行緒池中的執行緒名稱: 執行緒-5
1700012277893  執行緒池中的執行緒名稱: 執行緒-7
1700012277893  執行緒池中的執行緒名稱: 執行緒-8

可以執行緒池建立了 8 個執行緒去執行任務。在實際工作中,最大執行緒數會比例子中設定的 200 還要大,在預估最大執行緒數時要悲觀一些,儘量取較大的執行緒數。雖然遭遇執行緒數達到最大執行緒數時依舊不夠用的時候,可以透過執行緒池的 API 動態調整執行緒池配置,但是非常不優雅,而且操作難度比較高,不建議新手使用。

書的名字:從 Java 開始做效能測試

如果本書內容對你有所幫助,希望各位不吝讚賞,讓我可以貼補家用。讚賞兩位數可以提前閱讀未公開章節。我也會嘗試製作本書的影片教程,包括必要的答疑。

FunTester 原創精華

【連載】從 Java 開始效能測試

  • 混沌工程、故障測試、Web 前端
  • 服務端功能測試
  • 效能測試專題
  • Java、Groovy、Go
  • 白盒、工具、爬蟲、UI 自動化
  • 理論、感悟、影片
如果覺得我的文章對您有用,請隨意打賞。您的支援將鼓勵我繼續創作!
打賞支援
暫無回覆。

相關文章