執行緒池的五種狀態及建立執行緒池的幾種方式

隨風奔跑la發表於2020-10-30

在這裡插入圖片描述

上篇《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的成神之路,免費領取最新面試資料,技術電子書,架構進階相關資料等。
在這裡插入圖片描述

相關文章