我們每次執行一個多線程任務時用new Thread,這樣頻繁創建對象會導致系統性能差,線程也缺乏統一管理,可能會無限制新建線程,相互之間競爭導致系統資源耗盡,並且缺乏定時任務,中斷等功能。
線程池可以有效的提高系統資源的使用率,同時避免過多資源競爭,重用存在的線程,減少對象創建。
為了更好的控制多線程,JDK提供了一套線程框架Executor來幫助程序員有效的進行線程控制。Java.util.concurrent 包是專為 Java併發編程而設計的包,其中有一個比較重要的線程工廠類:Executors。
Java通過Executors創建不同功能的線程池,若Executors無法滿足需求,我們也可以創建自定義的線程池。本文分為以下部分講解:
- newFixedThreadPool()方法
- newSingleThreadExecutor()方法
- newCachedThreadPool()方法
- newScheduledThreadPool()方法
- 自定義線程池
在講述之前,因為上面5條在方法體內部其實是創建了ThreadPoolExecutor這個類的對象,所以我們先來看看ThreadPoolExecutor中線程執行任務的示意圖,它的執行任務分兩種情況:
1).Execute()方法會創建一個線程然後執行一個任務。
2).這個線程在執行完1之後,會反覆從BlockingQueue隊列(可以看我的上一篇文章【JDK併發包基礎】併發容器詳解)中獲取任務來執行。如果圖中所示三個線程同時間在執行任務,還有任務進來則會放入BlockingQueue隊列中暫緩起來等待線程空閒去執行。
再者,這3個線程正在使用,隊列也滿了的話(有界隊列的情況),還有任務進來,則會實行拒絕策略。(take()和poll()都是取頭元素節點,區別在於前者會刪除元素,後者不會)
1.newFixedThreadPool()方法
創建一個固定數量的線程池,裡面的線程數始終不變,當有一個線程提交時,若線程池中有空閒的線程,則立即執行。若沒有,則會暫緩在一個阻塞隊列LinkedBlockingQueue中等待有空閒的線程去執行。newFixedThreadPool()方法的源碼如下:
現在我們思考一下:假如有Thread1、Thread2、Thread3、Thread4四條線程分別統計C、D、E、F四個盤的大小,所有線程都統計完畢交給Thread5線程去做彙總,應當如何實現?
第一種方式是用join()來做,不推薦:
推薦使用線程池的方式:
運行結果如下,統計前四個盤大小可以沒有順序,但合計始終在最後:
2. newSingleThreadExecutor()方法
創建只有一個線程的線程池,若線程池中有空閒的線程,則立即執行。若沒有,則會暫緩在一個阻塞隊列LinkedBlockingQueue中等待有空閒的線程去執行,它保證所有任務按照提交順序執行。我們來看看newSingleThreadExecutor方法的源碼:
應用場景:這個線程池會在僅有的一個線程發生異常時,重新啟動一個線程來替代原來的線程執行下去。
3.newCachedThreadPool()方法
創建一個可根據實際情況調整線程個數的線程池,不限制線程數量。若有任務,則創建線程。若無任務,則不創建線程,並且每一個空閒的線程會在60秒後自動回收。我們來看看源碼:
源碼中的SynchronousQueue這個沒有容量的隊列一創建,內部就使用take()方法阻塞著,當有一個線程來了直接就執行。
4.newScheduledThreadPool()方法
創建一個大小無限的線程池,此線程池支持定時以及週期性執行任務的需求。它的源碼如下:
源碼中的DelayedWorkQueue是帶有延遲時間的一個隊列,其中元素只有當指定時間到了,才能夠從隊列中獲取元素,可以做定時的功能。
創建一個任務,等3秒初始化後每隔1秒打印一句話:
這個類似於Java的Timer定時器,但項目中用Quartz,跟Spring整合的話,最好用@Scheduled註解。ref:Spring Schedule 任務調度實現
5.自定義線程池
在上述Executors工廠類創建線程池時,它的創建線程方法內部實現均用了ThreadPoolExecutor這個類,ThreadPoolExecutor可以實現自定義線程池,它的構造方法如下:
這個構造方法對於BlockingQueue隊列是什麼類型比較關鍵,它關乎這個自定義線程池的功能。
1.使用有界隊列ArrayBlockingQueue時,實際線程數小於corePoolSize時,則創建線程。若大於corePoolSize時,則任務會加入BlockingQueue隊列中,若隊列已滿,則在實際線程總數不大於maximumPoolSize時,創建新線程。若還大於maximumPoolSize,則執行拒絕策略,或者自定義的其他方式。
2.使用無界隊列LinkedBlockingQueue時,緩衝隊列,當實際線程超過corePoolSize核心線程數後放置等待的線程,最後等系統空閒了在這個隊列裡取,maximumPoolSize參數在這裡就沒有作用了。因為它是無界隊列,所以除非系統資源耗盡,否則不會出現任務入隊失敗的情況。比如創建任務的速度和處理速度差異很大,無界隊列會保持快速增長,直到系統內存耗盡。
有界隊列和無界隊列實例如下:
用LinkedBlockingQueue無界隊列執行後結果是每過一段時間5個任務一執行:
對於拒絕策略,即當任務數量超過了系統實際承載能力時該如何處理呢?JDK提供了幾種實現策略:
AbortPolicy:直接拋出異常來阻止系統正常工作。
CallerRunsPolicy:只要線程池未關閉,會把丟棄的任務先執行。
DiscardOledestPolicy:丟棄最老的一個請求,嘗試再次提交當前任務
DiscardPolicy:丟棄無法處理的任務,不給於任何處理。
這四種策略個人覺得都不太好,我們可以實現一個自定義策略,在這裡實現RejectedExecutionHandler接口就好了:
運行結果如下:
到這裡已經介紹完了Java併發包下的線程池,博主是個普通的程序猿,水平有限,文章難免有錯誤,歡迎犧牲自己寶貴時間的讀者,就本文內容直抒己見。
系列:
【JDK併發包基礎】線程池詳解
【JDK併發包基礎】併發容器詳解
【JDK併發包基礎】工具類詳解
閱讀更多 Java是最好的語言NO1 的文章