詳解線程池的作用及Java中如何使用線程池



服務端應用程序(如數據庫和 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)會實現最高效率。

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

歡迎關注我的博客,里面有很多精品合集

  • 本文轉載注明出處(必須帶連接,不能只轉文字):字母哥博客

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM