JAVA-快速瞭解執行緒池的基本原理

我在風花雪月裡等你發表於2019-10-11

前言

       說起執行緒池大家肯定不會陌生,在面試中屬於必問的問題之一,特別是對於“高併發”有較高要求的企業,基本是必問點。網上關於執行緒池的文章和視訊很多,本篇文章旨在幫助大家快速瞭解和掌握執行緒池的基本原理,對於高階應用不過多涉及。



目錄

  1. 併發佇列
  2. 執行緒池簡介
  3. 為什麼需要執行緒池
  4. 執行緒池原理
  5. 執行緒池的分類



一、併發佇列

1. 併發佇列概念

       併發佇列是一個基於連結節點的無界執行緒安全佇列,它採用先進先出的規則對節點進行排序,當我們新增一個元素的時候,它會新增到佇列的尾部,當我們獲取一個元素時,它會返回佇列頭部的元素。

2. 併發佇列分類

       併發佇列分為阻塞佇列和非阻塞佇列,下面舉例示意:
       現有一個長度為10的佇列,有11個元素需要放進去
Alt
                                              示意圖

兩種佇列區別
  1. 入隊時
    非阻塞佇列:當向佇列中放入10個元素,此時佇列已滿,再放入第11個元素資料就會丟失。

    阻塞佇列:當佇列已滿了的時候,此時會進行等待,什麼時候佇列中有出隊的元素,那麼第11個再放進去。

  2. 出隊時
    非阻塞佇列:如果佇列中沒有元素了,此時進行出隊操作,往外取元素,得到的就是null。

    阻塞佇列:當佇列中沒有元素時,如果此時進行出隊操作會等待,什麼時候放進去,什麼時候再取出來。

       特別地,執行緒池就是基於阻塞佇列實現的。



二、執行緒池簡介


       執行緒池是一種多執行緒處理形式,處理過程中將任務新增到佇列,執行緒池在系統啟動時即建立大量空閒的執行緒,程式將一個任務傳給執行緒池,執行緒池就會啟動一條執行緒來執行這個任務。執行結束以後,該執行緒並不會死亡,而是再次返回執行緒池中成為空閒狀態,等待執行下一個任務。


       簡單來說,執行緒池就是執行緒的集合。




三、為什麼需要執行緒池


執行緒的正常生命週期如下圖所示:
在這裡插入圖片描述       為了便於分析,假設各階段所花時間如上所示(當然執行緒各階段實際所花時間極短,為毫秒級)。如果我們能省略其他階段,每次執行緒直接執行任務,這樣就可以單個執行緒處理任務就可以節省5秒。要實現這樣的設想,我們可以使用執行緒池來處理,因為執行緒池中的執行緒是事先建立好的大量空閒執行緒,當佇列中的任務進入執行緒池中,執行緒可以直接執行任務,執行完成後釋放資源,繼續處理下一任務。
       舉例來看:現有100個任務需要處理,一次最多建立10個執行緒。如果採用普通方式,一次建立10個執行緒處理10個任務,總共需60秒,而採用執行緒池的方式,一次執行10個任務,總共需要10秒。
       綜上所述:我們可以很明顯的看出執行緒池在處理任務量極大的高併發系統中,具有很大的優勢。



四、執行緒池的原理


1. ThreadPoolExecutor核心類

       執行緒池的最上層介面是Executor,這個介面定義了一個核心方法execute(Runnablecommand),這個方法是用來傳入任務的,最後被ThreadPoolExecutor類實現。而且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);
 
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnitunit,BlockingQueue<Runnable>workQueue,RejectedExecutionHandler handler);
 
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);


各個參數列示意義:

引數名 引數含義
corePoolSize 核心執行緒池大小,也即核心執行緒的數量
maximumPoolSize 最大執行緒池大小,也即執行緒的最大數量
keepAliveTime 空閒時間,是除核心執行緒之外的新建立執行緒的最大存活時間
TimeUnit 時間單位
workQueue 阻塞佇列,用來儲存等待的任務
threadFactory 執行緒工廠,用來建立新執行緒
handler 拒絕處理策略,當提交給執行緒池的任務量超過最大執行緒池大小+佇列長度,就會採取拒絕處理策略

特別地說明:
workQueue一般有以下三種阻塞佇列:
SynchronousQueue:直接提交,預設使用佇列
ArrayBlockingQueue:有界佇列
LinkedBlockingQueue:無界佇列

threadFactory是當佇列已滿,但執行緒總數量<最大執行緒池大小時,執行緒池中用來建立新執行緒的執行緒工廠。一般有下列三種型別:
ArrayBlockingQueue:有界執行緒安全的阻塞佇列。
LinkedBlockingQueue:併發安全的阻塞佇列。
SynchronousQueue:同步佇列。

handler觸發時,有以下四種拒絕處理策略:
hreadPoolExecutor.AbortPolicy(預設):丟棄任務並丟擲RejectedExecutionException異常。
ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不丟擲異常。
ThreadPoolExecutor.DiscardOldestPolicy:丟棄佇列最前面的任務,然後重新嘗試執行任務(重複此過程)
ThreadPoolExecutor.CallerRunsPolicy:由呼叫執行緒處理該任務



2.執行緒池原理圖

在這裡插入圖片描述

3. 執行緒池例項

       接下來就通過一個簡單的例項並結合原理圖來了解執行緒池的基本原理:


public class test02 {
    public static void main(String[] args) {
        ThreadPoolExecutor pool =
        new ThreadPoolExecutor(1,2,3, TimeUnit.SECONDS, new LinkedBlockingDeque<>(3));
        //利用執行緒池中的執行緒開始執行任務
        //執行第一個任務
        pool.execute(new TestThread());
        
        //佇列有三個任務等待
        pool.execute(new TestThread());
        pool.execute(new TestThread());
        pool.execute(new TestThread());
        
        //執行第五個任務
        pool.execute(new TestThread());
        //執行第六個任務,拒絕任務報錯
        //pool.execute(new TestThread());
        //當前執行緒池中有2個執行緒:1個核心執行緒 + 1個新建立的執行緒 = 最大執行緒數

        //關閉執行緒池
        pool.shutdown();
    }
}

class TestThread implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}


       首先建立一個最簡單型別的執行緒池,構造方法只有五個引數,每個引數意義如下:
1:核心執行緒數
2:最大執行緒數
3:空閒時間。新建立的執行緒執行任務後等待新任務的空閒時間
TimeUnit.SECONDS:時間單位,秒
new LinkedBlockingDeque:阻塞佇列,長度為3





不執行第6條任務時的執行結果如下:

在這裡插入圖片描述


執行第6條任務時執行結果如下:
在這裡插入圖片描述

分析程式碼執行過程:

在這裡插入圖片描述
       現有一執行緒池,裡面只有一個核心執行緒thread1,第一個任務進入執行緒池中,由thread1執行,而2-4號執行緒處在佇列中等待執行,當5號任務提交時,根據原理圖,此時滿足佇列已滿,且核+新<=最大,所以建立新執行緒thread2,由thread1和thread2分攤執行任務,由執行結果也可以看出,確實是分攤任務

       當加上第6條的任務時,根據原理圖,此時佇列已滿,且核+新>最大,沒有多餘的執行緒執行任務,佇列也無法裝入,就會報錯,拒絕任務。



五、執行緒池的分類


執行緒池可分為以下四類:


1. 可快取:newCachedThreadPool
  • 作用:建立一個根據需要建立新執行緒池的執行緒池。當舊執行緒釋放資源後就可以使用舊執行緒。

  • 特點:執行緒數靈活最大值為INTER.MAX_VALUE,底層採用一個近似無邊界佇列

2. 定長:newFixedThreadPool
  • 作用:建立一個可重用固定執行緒數的執行緒池,以共享的無界佇列來執行這些執行緒。

  • 特點:執行緒處於一定量,可以很好的控制併發量

3. 定時:newScheduleThreadPool
  • 作用:建立一個可延遲或延期執行的執行緒池。

  • 特點:執行緒池中具有指定數量的執行緒,可定時或延遲執行,適用於週期性執行任務的場景。

4. 單例:newSingleThreadExecutor
  • 作用:建立一個只有一個執行緒的執行緒池。且執行緒的存貨時間是無限的,當該執行緒正繁忙時,對於新任務會進入無界的阻塞佇列中。
  • 特點:適用於一個一個任務執行的場景。


版權宣告:

1.本部落格為原創的文章,版權歸原作者 我在風花雪月裡等你 所有;

2.未經原作者允許不得轉載本文內容,否則將視為侵權;

3.轉載或者引用本文內容請註明來源及原作者;

4.對於不遵守此宣告或者其他違法使用本文內容者,本人依法保留追究權等。


在這裡插入圖片描述

相關文章