執行緒池知識點詳解

萌新J發表於2020-11-22

引入

為什麼使用執行緒池?

  在連線數少的情況下,對於需要執行緒的地方我們只需要直接新建執行緒來處理就可以了,但是在併發量高的場景下,頻繁的執行緒建立、銷燬是非常消耗資源的,所以針對於這樣的場景可以使用執行緒池,讓一開始就建立好執行緒,在需要新連線進來需要執行緒時就從執行緒池中拿一條執行,完成後再將執行緒放回執行緒池,等到其他執行緒需要時再獲取就可以了,這樣可以有效提高系統整體的效能。

 

執行緒池的好處?適應場景?

  好處:1、降低資源損耗   2、響應速度快  3、方便執行緒管理  4、提供定時執行、定期執行、併發數控制等功能。適用場景:併發量大,IO操作多,需要頻繁建立執行緒的場景。

 

 

阻塞佇列

   阻塞佇列是一個支援兩個附加操作的佇列,其本質還是一個佇列,當內部儲存的資料量超過當前阻塞佇列的容量時,就會阻塞,停止接收新的資料;同樣,如果內部儲存的資料量變為0,那麼也會阻塞,外界的資料請求也會不再接收。

種類

1、ArrayBlockingQueue:由陣列結構組成的有界阻塞佇列

2、LinkedBlockingQueue:由連結串列組成的有界(但大小預設值為Integer.MAX_Value)

3、PriorityBlockingQueue:支援優先順序排序的無界阻塞佇列

4、DelayQueue:使用優先順序佇列實現的延遲無界阻塞佇列

5、SynchronizedQueue:不儲存元素的阻塞佇列,也即單個元素的佇列

6、LinkedTransferQueue:由連結串列結構組成的無界阻塞佇列

7、LinkedBlockingDeque:由連結串列結構組成的雙向阻塞佇列

其中橘色的三種是常用的。其中 LinkedBlockingQueue 和 SynchronizedQueue 是兩個極端,SynchronizedQueue 是沒有容量的阻塞佇列,而 LinkedBlockingQueue 在未指定容量時可以看作是容量無窮大的阻塞佇列。

 

核心方法

方法型別 丟擲異常 特殊值 阻塞 超時
插入   add(e) offer(e) put(e) offer(e,time,unit)
移除 remove() poll() take() poll(time,unit)
檢查 element() peek() 不可用 不可用

 

 

 

 

 

檢查是在有資料時返回佇列的第一個資料,並不會從佇列中移除該資料。內部使用 ReentrantLock 進行同步控制的。

丟擲異常

當阻塞佇列滿時,再往佇列裡add插入元素會拋lllegalStateException:Queue full

當阻塞佇列空時,再往佇列裡remove移除元素會拋NoSuchElementException

特殊值

插入方法,成功true失敗false

移除方法,成功返回出佇列的元素,沒有元素返回null

阻塞

佇列滿時put,佇列會一直阻塞直到put資料或者響應中斷退出

佇列為空時take,佇列會一直阻塞直到佇列可用

超時 當佇列滿時,佇列會阻塞生產者執行緒一定時間,超時後限時後生產者執行緒會退出

 

 

 

 

 

 

 

 

 

 

 

執行緒池

執行緒池的核心介面是 ExecutorService,它定義了執行緒池的各個基本抽象方法。

 

執行機制

當新的執行緒請求進來時,會先判斷核心執行緒數是否已滿,如果未滿則直接新建執行緒並執行,執行完將其放回執行緒池;

                           如果已滿就再檢查佇列是否已滿,如果沒滿就將當前執行緒請求加入阻塞佇列,等待空閒執行緒分配;

                                          如果已滿就再檢查執行緒池當前存在的執行緒數是否已達到規定的最大值,如果沒有達到就建立執行緒執行;

                                                                         如果達到就執行對應的飽和策略。

其中的名詞下面會解釋。

 

 

種類

ThreadPoolExecutor

首先看一下《阿里巴巴Java開發手冊》中推薦的執行緒池建立,這也是執行緒池的最基本建立方式。那就是使用建立 ThreadPoolExecutor 物件來作為執行緒池,首先看一下它的構造器

 可以看到它有四種建構函式,但前三種的實現本質還是使用第四種實現的,只不過使用的都是對應預設配置而已。

 所以我們著重看一下第四個建構函式。

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

內容是對應屬性的賦值,方法引數從左到右分別為核心執行緒數、執行緒池允許同時存在的最大執行緒數、執行緒的最大存活時間、最大存活時間的單位、阻塞佇列、執行緒工廠、拒絕策略。 

   核心執行緒數、最大執行緒數、阻塞佇列和拒絕策略就是上面執行機制中提到的。最大存活時間是指非核心執行緒在執行完程式碼後回到執行緒池,在經過最大存活時間後仍然沒有任務分配給它,那麼它就會被回收。核心執行緒則不會被回收,所以核心執行緒數就規定了執行緒池的最小執行緒數(當前執行緒池剛剛被建立時為0,直到有執行緒請求進來後才會開始建立執行緒)。執行緒工廠是指定建立執行緒的工廠,這樣在建立執行緒可以更方便。拒絕策略是指在阻塞佇列滿以及執行緒池容納的執行緒數也達到最大執行緒池後執行的策略。

拒絕策略包括以下幾種:
1、ThreadPoolExecutor.AbortPolicy:新任務提交直接丟擲異常,RejectedExecutionException(預設)

2、ThreadPoolExecutor.CallerRunsPolicy:即不拋棄任務也不丟擲異常,而是將任務返回給呼叫者。不會線上程池中執行,而是在呼叫executor方法的執行緒中執行(也就是傳進來的Runnble物件來建立執行緒啟動執行),會降低新任務的提交速度,影響程式的整體效能。

3、ThreadPoolExecutor.DiscardPolicy:直接拋棄新提交的任務。

4、ThreadPoolExecutor.DiscardOldestPolicy:拋棄最早加入阻塞佇列的請求。

需要注意的是這些拒絕策略其實是 ThreadPoolExecutor 的內部類。

 

關於核心執行緒數的設定,可以參考下面的配置

1、對於CPU密集型,也就是程式碼大部分操作都是CPU去執行計算處理的,不需要建立過多的執行緒,所以可以設定為 CPU核數+1

2、對於IO密集型,因為IO操作往往伴隨著執行緒的執行緒的使用,所以應該設定大一些,所以可以設定為 CPU核數*2

 

執行緒池的狀態

1、Running。執行中,執行緒池正常執行,當執行緒池被建立後就會進入 Running 狀態。

2、ShutDown。關閉,不會再接受新的執行緒請求,但是還是會處理阻塞佇列中的請求。當呼叫物件的 shutdown 方法後就會進入該狀態。

3、Stop。停止,不會再接受新的執行緒請求,也不會再處理阻塞佇列中的請求。當呼叫物件的 shutdownNow 方法後就會進入該狀態。

4、Tidying。進入該狀態會開始執行執行緒池的 terminated 方法。在 ShutDown 狀態中阻塞佇列為空,同時執行緒池中的工作執行緒數為0時就會進入該狀態;在 Stop 狀態中工作執行緒數為0就會進入該狀態。

5、Terminated。終止。表示執行緒池正式停止工作。當在 Tidying 狀態中執行完 terminated 方法後就會進入該狀態。

 

常用方法

1、execute(Runnable):處理 Ruunable 型別執行緒請求

2、submit(Runnable)、submit(Callable<T>):處理 Runnable 或者 Callable 型別的執行緒請求。submit 方法實現就是將 Callable物件轉成 FutureTask 型別物件再呼叫 execute 方法處理。

3、shutdown():進入 ShutDown 狀態

4、shutdownNow():進入 Stop 狀態

5、terminated():執行緒池停止前執行的方法,空方法,子類可以來重寫自定義。

6、getTaskCount():獲取執行緒池接收的任務總數

7、getCompletedTaskCount():獲取執行緒池已完成的任務數

8、getPoolSize():獲取執行緒池的執行緒數量

9、getActiveCount():獲取執行緒池正在執行的執行緒數

 

 

其他封裝好的執行緒池

  對於直接建立 ThreadPoolExecutor 物件來實現執行緒池的建立,過程比較複雜,當然在實際開發中還是推薦這種方式,而在某些場景中則不需要定義這麼規範的執行緒池,所以在 Executors 工具類中為我們封裝了幾種執行緒池,我們只需要呼叫方法就可以獲取對應的執行緒池。

1、Executors.newSingletonThreadExecutor。方法原始碼如下。

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

可以看到這個執行緒池就是一個單執行緒的執行緒池,只能儲存一個執行緒,但是它的阻塞佇列是 LinkedBlockingQueue,所以意味著阻塞佇列的容量可以看作是無限大的。

 

2、Executors.newFixedThreadPool(int)。方法原始碼如下。

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

這個執行緒池的最大執行緒數是傳參值,核心執行緒數也是傳參值,這意味著在工作執行緒執行完後回到執行緒池永遠不會被回收,使用的阻塞佇列也是 LinkedBlockingQueue。

 

3、Executors.newCachedThreadPool()。方法原始碼如下。

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

這裡的核心執行緒數是0,也就是執行緒池在空閒時會回收所有的執行緒,但是最大執行緒數是 Integer的最大範圍,所以可以看作可以同時包括無限大的執行緒,並且使用的阻塞佇列是 SynchronousQueue,所以當執行緒請求進來時總會立即建立執行緒執行。

 

4、Executors.newScheduledThreadPool(int)。方法原始碼如下。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }




 public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

這個執行緒池的核心執行緒數是指定的引數,最大執行緒數同樣是無限大,阻塞佇列是 DelayedWorkQueue(),預設容量是16。

 

上面四種封裝好的執行緒池都有缺陷,前兩個因為阻塞佇列是 LinkedBlockingQueue,所以在大量的執行緒請求進來時大部分會儲存在阻塞佇列中,最終撐爆堆空間,丟擲OOM;而後兩個因為允許的最大執行緒數是 Integer.MAX_VALUE,所以可以看作是無限大的,所以在大量的執行緒請求進來時也會因為建立過多的執行緒數而丟擲OOM。所以這四種執行緒池需要慎用。

相關文章