詳解線程池的作用及Java中如何使用線程池 一、什麼是 Java 中的線程池? 執行器線程池方法 二、線程池示例 創建線程池處理任務要遵循的步驟 示例執行結果 三、使用線程池的注意事項與調優

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

一、什麼是 Java 中的線程池?

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

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

上圖表示線程池初始化具有3 個線程,任務隊列中有5 個待運行的任務對象。

執行器線程池方法

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

二、線程池示例

在下面的內容中,我們將介紹線程池的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)會實現最高效率。

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

來源:https://www.tuicool.com/articles/fUNNJ3M

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章