Java線程池必知必會知識點總結

1、線程數使用開發規約

阿裡巴巴開發手冊中關於線程和線程池的使用有如下三條強制規約

【強制】創建線程或線程池時請指定有意義的線程名稱,方便出錯時回溯。

正例:自定義線程工廠,並且根據外部特征進行分組,比如,來自同一機房的調用,把機房編號賦值給whatFeatureOfGroup

public class UserThreadFactory implements ThreadFactory {
private final String namePrefix;
private final AtomicInteger nextId = new AtomicInteger(1);
/**
* 定義線程組名稱,在利用 jstack 來排查問題時,非常有幫助
*/

UserThreadFactory(String whatFeatureOfGroup) {
namePrefix = "From UserThreadFactory's " + whatFeatureOfGroup + "-Worker-";
}

@Override
public Thread newThread(Runnable task) {
String name = namePrefix + nextId.getAndIncrement();
Thread thread = new Thread(null, task, name, 0);
System.out.println(thread.getName());
return thread;
}
}

【強制】線程資源必須通過線程池提供,不允許在應用中自行顯式創建線程。

說明:線程池的好處是減少在創建和銷毀線程上所消耗的時間以及系統資源的開銷,解決資源不足的問題。

如果不使用線程池,有可能造成系統創建大量同類線程而導致消耗完內存或者“過度切換”的問題。

【強制】線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式,這

樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。

說明:Executors 返回的線程池對象的弊端如下:

1) FixedThreadPool 和 SingleThreadPool:

允許的請求隊列長度為 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。

2) CachedThreadPool:

允許的創建線程數量為 Integer.MAX_VALUE,可能會創建大量的線程,從而導致 OOM。

2、 ThreadPoolExecutor源碼

1. 構造函數

UML圖:

ThreadPoolExecutor的構造函數共有四個,但最終調用的都是同一個:

2.核心參數

  • corePoolSize => 線程池核心線程數量

  • maximumPoolSize => 線程池最大數量

  • keepAliveTime => 線程池的工作線程空閑後,保持存活的時間。如果任務多而且任務的執行時間比較短,可以調大keepAliveTime,提高線程的利用率。

  • unit => 時間單位

  • workQueue => 線程池所使用的緩沖隊列,隊列類型有:

    • ArrayBlockingQueue,基於數組結構的有界阻塞隊列,按FIFO(先進先出)原則對任務進行排序。使用該隊列,線程池中能創建的最大線程數為maximumPoolSize

    • LinkedBlockingQueue,基於鏈表結構的無界阻塞隊列,按FIFO(先進先出)原則對任務進行排序,吞吐量高於ArrayBlockingQueue。使用該隊列,線程池中能創建的最大線程數為corePoolSize。靜態工廠方法 Executor.newFixedThreadPool()使用瞭這個隊列。

    • SynchronousQueue,一個不存儲元素的阻塞隊列。添加任務的操作必須等到另一個線程的移除操作,否則添加操作一直處於阻塞狀態。靜態工廠方法 Executor.newCachedThreadPool()使用瞭這個隊列。

    • PriorityBlokingQueue:一個支持優先級的無界阻塞隊列。使用該隊列,線程池中能創建的最大線程數為corePoolSize。

  • threadFactory => 線程池創建線程使用的工廠

  • handler => 線程池對拒絕任務的處理策略,主要有4種類型的拒絕策略:

    • AbortPolicy:無法處理新任務時,直接拋出異常,這是默認策略。

    • CallerRunsPolicy:用調用者所在的線程來執行任務。

    • DiscardOldestPolicy:丟棄阻塞隊列中最靠前的一個任務,並執行當前任務。

    • DiscardPolicy:直接丟棄任務。

3.execute()方法

  • 如果當前運行的線程少於corePoolSize,則創建新的工作線程來執行任務(執行這一步驟需要獲取全局鎖)。

  • 如果當前運行的線程大於或等於corePoolSize,而且BlockingQueue未滿,則將任務加入到BlockingQueue中。

  • 如果BlockingQueue已滿,而且當前運行的線程小於maximumPoolSize,則創建新的工作線程來執行任務(執行這一步驟需要獲取全局鎖)。

  • 如果當前運行的線程大於或等於maximumPoolSize,任務將被拒絕,並調用RejectExecutionHandler.rejectExecution()方法。即調用飽和策略對任務進行處理。

3、線程池的工作流程

執行邏輯說明:

  • 判斷核心線程數是否已滿,核心線程數大小和corePoolSize參數有關,未滿則創建線程執行任務

  • 若核心線程池已滿,判斷隊列是否滿,隊列是否滿和workQueue參數有關,若未滿則加入隊列中

  • 若隊列已滿,判斷線程池是否已滿,線程池是否已滿和maximumPoolSize參數有關,若未滿創建線程執行任務

  • 若線程池已滿,則采用拒絕策略處理無法執執行的任務,拒絕策略和handler參數有關

4、Executors創建返回ThreadPoolExecutor對象(不推薦)

Executors創建返回ThreadPoolExecutor對象的方法共有三種:

1. Executors#newCachedThreadPool => 創建可緩存的線程池

  • corePoolSize => 0,核心線程池的數量為0

  • maximumPoolSize => Integer.MAX_VALUE,可以認為最大線程數是無限的

  • keepAliveTime => 60L

  • unit => 秒

  • workQueue => SynchronousQueue

弊端:maximumPoolSize => Integer.MAX_VALUE可能會導致OOM

2. Executors#newSingleThreadExecutor => 創建單線程的線程池

SingleThreadExecutor是單線程線程池,隻有一個核心線程:

  • corePoolSize => 1,核心線程池的數量為1

  • maximumPoolSize => 1,隻可以創建一個非核心線程

  • keepAliveTime => 0L

  • unit => 毫秒

  • workQueue => LinkedBlockingQueue

弊端:LinkedBlockingQueue是長度為Integer.MAX_VALUE的隊列,可以認為是無界隊列,因此往隊列中可以插入無限多的任務,在資源有限的時候容易引起OOM異常

3. Executors#newFixedThreadPool => 創建固定長度的線程池

  • corePoolSize => 1,核心線程池的數量為1

  • maximumPoolSize => 1,隻可以創建一個非核心線程

  • keepAliveTime => 0L

  • unit => 毫秒

  • workQueue => LinkedBlockingQueue

它和SingleThreadExecutor類似,唯一的區別就是核心線程數不同,並且由於使用的是LinkedBlockingQueue,在資源有限的時候容易引起OOM異常

5、線程池的合理配置

從以下幾個角度分析任務的特性:

  • 任務的性質:CPU 密集型任務、IO 密集型任務和混合型任務。

  • 任務的優先級:高、中、低。

  • 任務的執行時間:長、中、短。

  • 任務的依賴性:是否依賴其他系統資源,如數據庫連接。

任務性質不同的任務可以用不同規模的線程池分開處理。可以通過 Runtime.getRuntime().availableProcessors()方法獲得當前設備的 CPU 個數。

  • CPU 密集型任務:配置盡可能小的線程,如配置 cpu核心數+1 個線程的線程池。

  • IO 密集型任務 :由於線程並不是一直在執行任務,則配置盡可能多的線程,如2 ∗ Ncpu

  • 混合型任務:如果可以拆分,則將其拆分成一個 CPU 密集型任務和一個 IO 密集型任務。隻要這兩個任務執行的時間相差不是太大,那麼分解後執行的吞吐率要高於串行執行的吞吐率;如果這兩個任務執行時間相差太大,則沒必要進行分解。

優先級不同的任務可以使用優先級隊列 PriorityBlockingQueue 來處理,它可以讓優先級高的任務先得到執行。但是,如果一直有高優先級的任務加入到阻塞隊列中,那麼低優先級的任務可能永遠不能執行。

執行時間不同的任務可以交給不同規模的線程池來處理,或者也可以使用優先級隊列,讓執行時間短的任務先執行。

依賴數據庫連接池的任務,因為線程提交 SQL 後需要等待數據庫返回結果,線程數應該設置得較大,這樣才能更好的利用 CPU。

建議使用有界隊列,有界隊列能增加系統的穩定性和預警能力。可以根據需要設大一點,比如幾千。使用無界隊列,線程池的隊列就會越來越大,有可能會撐滿內存,導致整個系統不可用。

處理拒絕策略有以下幾種比較推薦:

在程序中捕獲RejectedExecutionException異常,在捕獲異常中對任務進行處理。針對默認拒絕策略使用CallerRunsPolicy拒絕策略,該策略會將任務交給調用execute的線程執行【一般為主線程】,此時主線程將在一段時間內不能提交任何任務,從而使工作線程處理正在執行的任務。此時提交的線程將被保存在TCP隊列中,TCP隊列滿將會影響客戶端,這是一種平緩的性能降低自定義拒絕策略,隻需要實現RejectedExecutionHandler接口即可如果任務不是特別重要,使用DiscardPolicy和DiscardOldestPolicy拒絕策略將任務丟棄也是可以的如果使用Executors的靜態方法創建ThreadPoolExecutor對象,可以通過使用Semaphore對任務的執行進行限流也可以避免出現OOM異常。

6、拒絕策略

有以下幾種比較推薦:

  • 在程序中捕獲RejectedExecutionException異常,在捕獲異常中對任務進行處理。針對默認拒絕策略

  • 使用CallerRunsPolicy拒絕策略,該策略會將任務交給調用execute的線程執行【一般為主線程】,此時主線程將在一段時間內不能提交任何任務,從而使工作線程處理正在執行的任務。此時提交的線程將被保存在TCP隊列中,TCP隊列滿將會影響客戶端,這是一種平緩的性能降低

  • 自定義拒絕策略,隻需要實現RejectedExecutionHandler接口即可

  • 如果任務不是特別重要,使用DiscardPolicy和DiscardOldestPolicy拒絕策略將任務丟棄也是可以的如果使用Executors的靜態方法創建ThreadPoolExecutor對象,可以通過使用Semaphore對任務的執行進行限流也可以避免出現OOM異常。

  • 參考文章:8大拒絕策略

7、線程池的五種運行狀態

線程狀態:

不同於線程狀態,線程池也有如下幾種 狀態:

RUNNING :該狀態的線程池既能接受新提交的任務,又能處理阻塞隊列中任務。

SHUTDOWN:該狀態的線程池不能接收新提交的任務,但是能處理阻塞隊列中的任務。(政府服務大廳不在允許群眾拿號瞭,處理完手頭的和排隊的政務就下班)

處於 RUNNING 狀態時,調用 shutdown()方法會使線程池進入到該狀態。

註意:finalize() 方法在執行過程中也會隱式調用shutdown()方法。

STOP:該狀態的線程池不接受新提交的任務,也不處理在阻塞隊列中的任務,還會中斷正在執行的任務。(政府服務大廳不再進行服務瞭,拿號、排隊、以及手頭工作都停止瞭。)

在線程池處於 RUNNING 或 SHUTDOWN 狀態時,調用shutdownNow() 方法會使線程池進入到該狀態;

TIDYING:如果所有的任務都已終止,workerCount (有效線程數)=0。

線程池進入該狀態後會調用 terminated() 鉤子方法進入TERMINATED 狀態。

TERMINATED:在terminated()鉤子方法執行完後進入該狀態,默認terminated()鉤子方法中什麼也沒有做。

【參考文章】

【1】《JAVA並發編程藝術》

【2】tech.meituan.com/2020/04/02/…

總結

到此這篇關於Java線程池必知必會知識點的文章就介紹到這瞭,更多相關Java線程池必知必會內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: