Java線程池那些事兒

阿里巴巴Java手冊中,關於線程池:

  • 線程資源必須通過線程池提供,不允許在應用中自行顯示創建線程。
  • 使用線程池的好處,是減少在創建和銷燬線程上所花的時間以及系統資源的開銷,解決資源不足的問題。
  • 如果不使用線程池,有可能造成系統創建大量同類線程而導致消耗完內存或者“過度切換”的問題。

 

線程池的好處:

  • 可以重用線程,避免線程創建的開銷;
  • 任務過多時,通過排隊避免創建過多線程,減少系統資源消耗和競爭,確保任務有序完成。

一、JUC線程池詳解

Java JUC包中的實現類是ThreadPoolExecutor,繼承AbstractExecutorService,實現了ExecutorService。

ThreadPoolExecutor比較重要的兩個構造方法:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue)
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) 

其中:

  • corePoolSize : 核心線程數量
  • maximumPoolSize : 最大線程數量
  • keepAliveTime / unit : 空閒線程存活時間
  • workQueue : 任務隊列
  • threadFactory : 線程工廠
  • handler : 拒絕策略

 

1、corePoolSize,核心線程數量,剛創建一個線程池後,不會創建任何線程。

當有新任務到來時,如果當前線程數量小於corePoolSize,會創建一個新線程執行該任務,即使其他線程是空閒的,也會創建新線程。如果線程數量大於corePoolSize,不會立即創建新線程,而是嘗試排隊,如果因爲隊列滿了或其他原因不能立即入隊,就不會排隊,而是檢查線程個數是否達到了maximumPoolSize,如果沒有達到,繼續創建新線程,直到線程數達到maximumPoolSize。

流程圖

核心線程:當線程個數小於corePoolSize時的線程。

核心線程默認行爲:

  • 不會預先創建,只有當有任務時纔會創建。
  • 不會因爲空閒而被終止,keepAliveTime參數不適用核心線程。

改變這些默認行爲ThreadPoolExecutor有如下方法:

// 預先創建所有核心線程
public int prestartAllCoreThreads();
// 創建一個核心線程,如果所有核心線程都已經創建,返回false
public boolean prestartCoreThread();
// 參數爲true時,允許keepAliveTime適用於核心線程
public void allowCoreThreadTimeOut(boolean value)

 

2、keepAliveTime,目的是爲了釋放多餘的線程資源。當線程池中線程個數大於corePoolSize時額外空閒線程的存活時間。一個非核心線程,在空閒等待新任務的最長等待時間。0表示所有線程都不會超時終止。

 

3、workQueue,阻塞隊列BlockingQueue:

  • LinkedBlockingQueue:基於鏈表,可以指定最大長度,默認無界
  • ArrayBlockingQueue:基於數組,有界
  • PriorityBlockingQueue:基於堆,無界阻塞優先級隊列
  • SynchronousQueue:沒有實際存儲空間的同步阻塞隊列。

對於無界隊列,線程個數最多隻能達到corePoolSize,達到corePoolSize後,新任務總會排隊,maximumPoolSize就沒有意義了。

對於SynchronousQueue,當嘗試排隊時,只有正好有空閒線程在等待接受任務時,纔會入隊成功,否則,總是會創建新線程,直到maximumPoolSize。

 

4、handler,RejectedExecutionHandler,任務拒絕策略

隊列有限,並且maximumPoolSize有限,當隊列排滿,線程個數也達到maximumPoolSize,此時新任務會觸發線程池的任務拒絕策略。

ThreadPoolExecutor實現了4種處理方式:

  • ThreadPoolExecutor.AbortPolicy:默認方式,拋出異常
  • ThreadPoolExecutor.DiscardPolicy:靜默處理,忽略新任務,不拋出異常,也不執行
  • ThreadPoolExecutor.DiscardOldestPolicy:將等待時間最長的任務扔掉,然後新任務入隊
  • ThreadPoolExecutor.CallerRunPolicy:在任務提交者線程中執行新任務,而不是交給線程池的線程執行。

拒絕策略只有在隊列有界,maximumPoolSize有限的情況下才會觸發。

如果隊列無界,服務不了的任務總是會排隊;請求隊列可能會消耗非常大的內存,甚至引發OOM;

如果隊列有界但maximumPoolSize無限,可能會創建過多的線程,佔滿CPU和內存,使得任何任務都難以完成。

在任務量非常大的場景中,需要讓拒絕策略有機會執行。

 

5、threadFactory,線程工廠

ThreadPoolExecutor的默認實現是Executors類中的靜態內部類DefaultThreadFactory。

創建一個線程,設置默認名稱(pool-線程池編號-thread-線程編號),設置daemon屬性爲false,設置線程優先級爲標準默認優先級(5)。

如果要自定義線程屬性,可以實現自定義的ThreadFactory。

 

二、工廠類Executors

雖然不推薦直接使用Executors工廠類創建線程池,但還是要了解一下利弊。

1.newSingleThreadExecutor

只使用一個線程,使用無界隊列,線程創建後不會超時,順序執行所有任務。

適用於需要保證所有任務被順序執行的場合。

無界隊列,如果排隊任務過多,可能會消耗過多的內存。

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

 

2.newFixedThreadPool

使用固定樹木的線程,使用無界隊列,線程創建後不會超時終止。

無界隊列,如果排隊任務過多,可能會消耗過多的內存。

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

 

3.newCachedThreadPool

核心線程數爲0,最大線程數爲Integer的最大值,線程空閒時間爲60秒,隊列爲SynchronousQueue。

當新任務提交,正好有空閒線程在等待任務,則空閒線程接受該任務,否則總是創建新線程。對任一空閒線程60s內沒有新任務則終止。

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

 

使用場景對比:

  • 系統負載很高 - newFixedThreadPool
    • newFixedThreadPool,通過隊列對新任務排隊,保證有足夠的資源處理實際任務;
    • newCachedThreadPool,爲每個任務創建一個線程,導致創建過多的線程,競爭CPU和內存資源;
  • 系統負載不太高,單個任務執行時間比較短 - newCachedThreadPool
    • newCachedThreadPool,效率可能更高,因爲任務可以不經排隊,直接交給一個空閒線程或新建線程。
  • 系統負載可能極高 - 兩者都不是最好的選擇,應根據具體情況自定義合適的參數。
    • newFixedThreadPool,隊列過長
    • newCachedThreadPool,線程過多
  • CPU密集型任務(計算型任務),一般線程數量爲CPU數量的1~2倍,過多線程可能增大上下文切換的開銷。
  • IO密集型任務,相對比CPU密集型任務,需要多一些線程,根據具體的IO阻塞時長進行考量決定。如tomcat,默認最大線程數爲200。

 

網上的帖子質量參差不齊,甚至有些前後矛盾,在下的文章都是學習過程中的總結,如果發現錯誤,歡迎留言指出~

 

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