詳解執行緒池的作用及Java中如何使用執行緒池

字母哥部落格發表於2021-01-14


服務端應用程式(如資料庫和 Web 伺服器)需要處理來自客戶端的高併發、耗時較短的請求任務,所以頻繁的建立處理這些請求的所需要的執行緒就是一個非常消耗資源的操作。常規的方法是針對一個新的請求建立一個新執行緒,雖然這種方法似乎易於實現,但它有重大缺點。為每個請求建立新執行緒將花費更多的時間,在建立和銷燬執行緒時花費更多的系統資源。因此同時建立太多執行緒的 JVM 可能會導致系統記憶體不足,這就需要限制要建立的執行緒數,也就是需要使用到執行緒池。

一、什麼是 Java 中的執行緒池?

執行緒池技術就是執行緒的重用技術,使用之前建立好的執行緒來執行當前任務,並提供了針對執行緒週期開銷和資源衝突問題的解決方案。 由於請求到達時執行緒已經存在,因此消除了執行緒建立過程導致的延遲,使應用程式得到更快的響應。

  • Java提供了以Executor介面及其子介面ExecutorServiceThreadPoolExecutor為中心的執行器框架。通過使用Executor,完成執行緒任務只需實現 Runnable介面並將其交給執行器執行即可。
  • 為您封裝好執行緒池,將您的程式設計任務側重於具體任務的實現,而不是執行緒的實現機制。
  • 若要使用執行緒池,我們首先建立一個 ExecutorService物件,然後向其傳遞一組任務。ThreadPoolExcutor 類則可以設定執行緒池初始化和最大的執行緒容量。

上圖表示執行緒池初始化具有3 個執行緒,任務佇列中有5 個待執行的任務物件。

執行器執行緒池方法

方法 描述
newFixedThreadPool(int) 建立具有固定的執行緒數的執行緒池,int參數列示執行緒池內執行緒的數量
newCachedThreadPool() 建立一個可快取執行緒池,該執行緒池可靈活回收空閒執行緒。若無空閒執行緒,則新建執行緒處理任務。
newSingleThreadExecutor() 建立一個單執行緒化的執行緒池,它只會用唯一的工作執行緒來執行任務
newScheduledThreadPool 建立一個定長執行緒池,支援定時及週期性任務執行

在固定執行緒池的情況下,如果執行器當前執行的所有執行緒,則掛起的任務將放在佇列中,並線上程變為空閒時執行。

二、執行緒池示例

在下面的內容中,我們將介紹執行緒池的executor執行器。

建立執行緒池處理任務要遵循的步驟

  1. 建立一個任務物件(實現Runnable介面),用於執行具體的任務邏輯
  2. 使用Executors建立執行緒池ExecutorService
  3. 將待執行的任務物件交給ExecutorService進行任務處理
  4. 停掉 Executor 執行緒池
//第一步: 建立一個任務物件(實現Runnable介面),用於執行具體的任務邏輯 (Step 1) 
class Task implements Runnable  {
    private String name;

    public Task(String s) {
        name = s;
    }

    // 列印任務名稱並Sleep 1秒
    // 整個處理流程執行5次
    public void run() {
        try{
            for (int i = 0; i<=5; i++) {
                if (i==0) {
                    Date d = new Date();
                    SimpleDateFormat ft = new SimpleDateFormat("hh:mm:ss");
                    System.out.println("任務初始化" + name +" = " + ft.format(d));
                    //第一次執行的時候,列印每一個任務的名稱及初始化的時間
                }
                else{
                    Date d = new Date();
                    SimpleDateFormat ft = new SimpleDateFormat("hh:mm:ss");
                    System.out.println("任務正在執行" + name +" = " + ft.format(d));
                    // 列印每一個任務處理的執行時間
                }
                Thread.sleep(1000);
            }
            System.out.println("任務執行完成" + name);
        }  catch(InterruptedException e)  {
            e.printStackTrace();
        }
    }
}

測試用例

public class ThreadPoolTest {
    // 執行緒池裡面最大執行緒數量
    static final int MAX_SIZE = 3;

    public static void main (String[] args) {
        // 建立5個任務
        Runnable r1 = new Task("task 1");
        Runnable r2 = new Task("task 2");
        Runnable r3 = new Task("task 3");
        Runnable r4 = new Task("task 4");
        Runnable r5 = new Task("task 5");

        // 第二步:建立一個固定執行緒數量的執行緒池,執行緒數為MAX_SIZE
        ExecutorService pool = Executors.newFixedThreadPool(MAX_SIZE);

        // 第三步:將待執行的任務物件交給ExecutorService進行任務處理
        pool.execute(r1);
        pool.execute(r2);
        pool.execute(r3);
        pool.execute(r4);
        pool.execute(r5);

        // 第四步:關閉執行緒池
        pool.shutdown();
    }
} 

示例執行結果

任務初始化task 1 = 05:25:55
任務初始化task 2 = 05:25:55
任務初始化task 3 = 05:25:55
任務正在執行task 3 = 05:25:56
任務正在執行task 1 = 05:25:56
任務正在執行task 2 = 05:25:56
任務正在執行task 1 = 05:25:57
任務正在執行task 3 = 05:25:57
任務正在執行task 2 = 05:25:57
任務正在執行task 3 = 05:25:58
任務正在執行task 1 = 05:25:58
任務正在執行task 2 = 05:25:58
任務正在執行task 2 = 05:25:59
任務正在執行task 3 = 05:25:59
任務正在執行task 1 = 05:25:59
任務正在執行task 1 = 05:26:00
任務正在執行task 2 = 05:26:00
任務正在執行task 3 = 05:26:00
任務執行完成task 3
任務執行完成task 2
任務執行完成task 1
任務初始化task 5 = 05:26:01
任務初始化task 4 = 05:26:01
任務正在執行task 4 = 05:26:02
任務正在執行task 5 = 05:26:02
任務正在執行task 4 = 05:26:03
任務正在執行task 5 = 05:26:03
任務正在執行task 5 = 05:26:04
任務正在執行task 4 = 05:26:04
任務正在執行task 4 = 05:26:05
任務正在執行task 5 = 05:26:05
任務正在執行task 4 = 05:26:06
任務正在執行task 5 = 05:26:06
任務執行完成task 4
任務執行完成task 5

如程式執行結果中顯示的一樣,任務 4 或任務 5 僅在池中的執行緒變為空閒時才執行。在此之前,額外的任務將放在待執行的佇列中。

執行緒池執行前三個任務,執行緒池內執行緒回收空出來之後再去處理執行任務 4 和 5

使用這種執行緒池方法的一個主要優點是,假如您希望一次處理10000個請求,但不希望建立10000個執行緒,從而避免造成系統資源的過量使用導致的當機。您可以使用此方法建立一個包含500個執行緒的執行緒池,並且可以向該執行緒池提交500個請求。
ThreadPool此時將建立最多500個執行緒,一次處理500個請求。在任何一個執行緒的程式完成之後,ThreadPool將在內部將第501個請求分配給該執行緒,並將繼續對所有剩餘的請求執行相同的操作。在系統資源比較緊張的情況下,執行緒池是保證程式穩定執行的一個有效的解決方案。

三、使用執行緒池的注意事項與調優

  1. 死鎖: 雖然死鎖可能發生在任何多執行緒程式中,但執行緒池引入了另一個死鎖案例,其中所有執行執行緒都在等待佇列中某個阻塞執行緒的執行結果,導致執行緒無法繼續執行。
  2. 執行緒洩漏 : 如果執行緒池中執行緒在任務完成時未正確返回,將發生執行緒洩漏問題。例如,某個執行緒引發異常並且池類沒有捕獲此異常,則執行緒將異常退出,從而執行緒池的大小將減小一個。如果這種情況重複多次,則執行緒池最終將變為空,沒有執行緒可用於執行其他任務。
  3. 執行緒頻繁輪換: 如果執行緒池大小非常大,則執行緒之間進行上下文切換會浪費很多時間。所以在系統資源允許的情況下,也不是執行緒池越大越好。

執行緒池大小優化: 執行緒池的最佳大小取決於可用的處理器數量和待處理任務的性質。對於CPU密集型任務,假設系統有N個邏輯處理核心,N 或 N+1 的最大執行緒池數量大小將實現最大效率。對於 I/O密集型任務,需要考慮請求的等待時間(W)和服務處理時間(S)的比例,執行緒池最大大小為 N*(1+ W/S)會實現最高效率。

不要教條的使用上面的總結,需要根據自己的應用任務處理型別進行靈活的設定與調優,其中少不了測試實驗。

歡迎關注我的部落格,裡面有很多精品合集

  • 本文轉載註明出處(必須帶連線,不能只轉文字):字母哥部落格

覺得對您有幫助的話,幫我點贊、分享!您的支援是我不竭的創作動力! 。另外,筆者最近一段時間輸出瞭如下的精品內容,期待您的關注。

相關文章