上篇《Java線程的6種狀態詳解及創建線程的4種方式》
前言:我們都知道,線程是稀有資源,系統頻繁創建會很大程度上影響服務器的使用效率,如果不加以限制,很容易就會把服務器資源耗盡。所以,我們可以通過創建線程池來管理這些線程,提升對線程的使用率。
1、什么是線程池?
簡而言之,線程池就是管理線程的一個容器,有任務需要處理時,會相繼判斷核心線程數是否還有空閑、線程池中的任務隊列是否已滿、是否超過線程池大小,然后調用或創建線程或者排隊,線程執行完任務后並不會立即被銷毀,而是仍然在線程池中等待下一個任務,如果超過存活時間還沒有新的任務就會被銷毀,通過這樣復用線程從而降低開銷。
2、使用線程池有什么優點?
可能有人就會問了,使用線程池有什么好處嗎?那不用說,好處自然是有滴。大概有以下:
1、提升線程池中線程的使用率,減少對象的創建、銷毀。
2、線程池的伸縮性對性能有較大的影響,使用線程池可以控制線程數,有效的提升服務器的使用資源,避免由於資源不足而發生宕機等問題。(創建太多線程,將會浪費一定的資源,有些線程未被充分使用;銷毀太多線程,將導致之后浪費時間再次創建它們;創建線程太慢,將會導致長時間的等待,性能變差;銷毀線程太慢,導致其它線程資源飢餓。)
3、線程池的核心工作流程(重要)
我們要使用線程池得先了解它是怎么工作的,流程如下圖,廢話不多說看圖就行。核心就是復用線程,降低開銷。
4、線程池的五種狀態生命周期
- RUNNING :能接受新提交的任務,並且也能處理阻塞隊列中的任務。
- SHUTDOWN:關閉狀態,不再接受新提交的任務,但卻可以繼續處理阻塞隊列中已保存的任務。在線程池處於 RUNNING 狀態時,調用 shutdown() 方法會使線程池進入到該狀態。(finalize() 方法在執行過程中也會調用 shutdown() 方法進入該狀態)。
- STOP:不能接受新任務,也不處理隊列中的任務,會中斷正在處理任務的線程。在線程池處於 RUNNING 或 SHUTDOWN 狀態時,調用 shutdownNow() 方法會使線程池進入到該狀態。
- TIDYING:如果所有的任務都已終止了,workerCount (有效線程數) 為0,線程池進入該狀態后會調用 terminated() 方法進入 TERMINATED 狀態。
- TERMINATED:在 terminated() 方法執行完后進入該狀態,默認 terminated() 方法中什么也沒有做。
5、創建線程池的幾種方式
- 通過 Executors 工廠方法創建
- 通過 new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue
workQueue) 自定義創建
相對而言,更建議用第二個創建線程池,Executors 創建的線程池內部很多地方用到了無界任務隊列,在高並發場景下,無界任務隊列會接收過多的任務對象,嚴重情況下會導致 JVM 崩潰,一些大廠也是禁止使用 Executors 工廠方法去創建線程池。newFixedThreadPool 和 newSingleThreadExecutor 的主要問題是堆積的請求處理隊列可能會耗費非常大的內存,甚至 OOM;newCachedThreadPool 和 newScheduledThreadPool 的主要問題是線程數最大數是 Integer.MAX_VALUE,可能會創建數量非常多的線程,甚至 OOM。
5.1、Executors 五個工廠方法創建不同線程池的區別
1、newCachedThreadPool()(工作隊列使用的是 SynchronousQueue)
創建一個線程池,如果線程池中的線程數量過大,它可以有效的回收多余的線程,如果線程數不足,那么它可以創建新的線程。
不足:這種方式雖然可以根據業務場景自動的擴展線程數來處理我們的業務,但是最多需要多少個線程同時處理卻是我們無法控制的。
優點:如果當第二個任務開始,第一個任務已經執行結束,那么第二個任務會復用第一個任務創建的線程,並不會重新創建新的線程,提高了線程的復用率。
作用:該方法返回一個可以根據實際情況調整線程池中線程的數量的線程池。即該線程池中的線程數量不確定,是根據實際情況動態調整的。
2、newFixedThreadPool()(工作隊列使用的是 LinkedBlockingQueue)
這種方式可以指定線程池中的線程數。如果滿了后又來了新任務,此時只能排隊等待。
優點:newFixedThreadPool 的線程數是可以進行控制的,因此我們可以通過控制最大線程來使我們的服務器達到最大的使用率,同時又可以保證即使流量突然增大也不會占用服務器過多的資源。
作用:該方法返回一個固定線程數量的線程池,該線程池中的線程數量始終不變,即不會再創建新的線程,也不會銷毀已經創建好的線程,自始自終都是那幾個固定的線程在工作,所以該線程池可以控制線程的最大並發數。
3、newScheduledThreadPool()
該線程池支持定時,以及周期性的任務執行,我們可以延遲任務的執行時間,也可以設置一個周期性的時間讓任務重復執行。該線程池中有以下兩種延遲的方法。
scheduleAtFixedRate 不同的地方是任務的執行時間,如果間隔時間大於任務的執行時間,任務不受執行時間的影響。如果間隔時間小於任務的執行時間,那么任務執行結束之后,會立馬執行,至此間隔時間就會被打亂。
scheduleWithFixedDelay 的間隔時間不會受任務執行時間長短的影響。
作用:該方法返回一個可以控制線程池內線程定時或周期性執行某任務的線程池。
4、newSingleThreadExecutor()
這是一個單線程池,至始至終都由一個線程來執行。
作用:該方法返回一個只有一個線程的線程池,即每次只能執行一個線程任務,多余的任務會保存到一個任務隊列中,等待這一個線程空閑,當這個線程空閑了再按 FIFO 方式順序執行任務隊列中的任務。
5、newSingleThreadScheduledExecutor()
只有一個線程,用來調度任務在指定時間執行。
作用:該方法返回一個可以控制線程池內線程定時或周期性執行某任務的線程池。只不過和上面的區別是該線程池大小為 1,而上面的可以指定線程池的大小。
使用示例:
//創建一個會根據需要創建新線程的線程池
ExecutorService executor= Executors.newCachedThreadPool();
for (int i = 0; i < 20; i++) {
executor.submit(new Runnable() {
@Override
public void run() {
System.out.println(i);
}
});
}
這五種線程池都是直接或者間接獲取的 ThreadPoolExecutor 實例 ,只是實例化時傳遞的參數不一樣。所以如果 Java 提供的線程池滿足不了我們的需求,我們可以通過 ThreadPoolExecutor 構造方法創建自定義線程池。
5.2、ThreadPoolExecutor 構造方法參數詳解
public ThreadPoolExecutor(
int corePoolSize,//線程池核心線程大小
int maximumPoolSize,//線程池最大線程數量
long keepAliveTime,//空閑線程存活時間
TimeUnit unit,//空閑線程存活時間單位,一共有七種靜態屬性(TimeUnit.DAYS天,TimeUnit.HOURS小時,TimeUnit.MINUTES分鍾,TimeUnit.SECONDS秒,TimeUnit.MILLISECONDS毫秒,TimeUnit.MICROSECONDS微妙,TimeUnit.NANOSECONDS納秒)
BlockingQueue<Runnable> workQueue,//工作隊列
ThreadFactory threadFactory,//線程工廠,主要用來創建線程(默認的工廠方法是:Executors.defaultThreadFactory()對線程進行安全檢查並命名)
RejectedExecutionHandler handler//拒絕策略(默認是:ThreadPoolExecutor.AbortPolicy不執行並拋出異常)
)
使用示例:
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 20, 2, TimeUnit.SECONDS, new LinkedBlockingQueue<>(5));
5.2.1、工作隊列
jdk 中提供了四種工作隊列:
①ArrayBlockingQueue
基於數組的有界阻塞隊列,按 FIFO 排序。新任務進來后,會放到該隊列的隊尾,有界的數組可以防止資源耗盡問題。當線程池中線程數量達到 corePoolSize 后,再有新任務進來,則會將任務放入該隊列的隊尾,等待被調度。如果隊列已經是滿的,則創建一個新線程,如果線程數量已經達到 maxPoolSize,則會執行拒絕策略。
②LinkedBlockingQuene
基於鏈表的無界阻塞隊列(其實最大容量為 Interger.MAX_VALUE),按照 FIFO 排序。由於該隊列的近似無界性,當線程池中線程數量達到 corePoolSize 后,再有新任務進來,會一直存入該隊列,而不會去創建新線程直到 maxPoolSize,因此使用該工作隊列時,參數 maxPoolSize 其實是不起作用的。
③SynchronousQuene
一個不緩存任務的阻塞隊列,生產者放入一個任務必須等到消費者取出這個任務。也就是說新任務進來時,不會緩存,而是直接被調度執行該任務,如果沒有可用線程,則創建新線程,如果線程數量達到 maxPoolSize,則執行拒絕策略。
④PriorityBlockingQueue
具有優先級的無界阻塞隊列,優先級通過參數 Comparator 實現。
5.2.2、拒絕策略
當工作隊列中的任務已到達最大限制,並且線程池中的線程數量也達到最大限制,這時如果有新任務提交進來,就會執行拒絕策略。jdk中提供了4中拒絕策略:
①ThreadPoolExecutor.CallerRunsPolicy
該策略下,在調用者線程中直接執行被拒絕任務的 run 方法,除非線程池已經 shutdown,則直接拋棄任務。
②ThreadPoolExecutor.AbortPolicy
該策略下,直接丟棄任務,並拋出 RejectedExecutionException 異常。
③ThreadPoolExecutor.DiscardPolicy
該策略下,直接丟棄任務,什么都不做。
④ThreadPoolExecutor.DiscardOldestPolicy
該策略下,拋棄進入隊列最早的那個任務,然后嘗試把這次拒絕的任務放入隊列。
除此之外,還可以根據應用場景需要來實現 RejectedExecutionHandler 接口自定義策略。
6、線程池的關閉
- shutdown():
1、調用之后不允許繼續往線程池內添加線程;
2、線程池的狀態變為 SHUTDOWN 狀態;
3、所有在調用 shutdown() 方法之前提交到 ExecutorSrvice 的任務都會執行;
4、一旦所有線程結束執行當前任務,ExecutorService 才會真正關閉。 - shutdownNow():
1、該方法返回尚未執行的 task 的 List;
2、線程池的狀態變為 STOP 狀態;
3、嘗試停止所有的正在執行或暫停任務的線程。
簡單點來說,就是:
shutdown() 調用后,不可以再 submit 新的 task,已經 submit 的將繼續執行
shutdownNow() 調用后,試圖停止當前正在執行的 task,並返回尚未執行的 task 的 list
7、總結
本文簡單介紹了線程池的一些相關知識,相信大家對線程池的優點,線程池的生命周期,線程池的工作流程及線程池的使用有了一個大概的了解,也希望能對有需要的人提供一點幫助!文中有錯誤的地方,還請留言給予指正,謝謝~
也歡迎大家關注我的公眾號:Java的成神之路,免費領取最新面試資料,技術電子書,架構進階相關資料等。