別再說你不懂執行緒池——做個優雅的攻城獅

低至一折起發表於2018-02-08
  • 什麼是執行緒池
  • 為什麼要使用執行緒池
  • 執行緒池的處理邏輯
  • 如何使用執行緒池
  • 如何合理配置執行緒池的大小
  • 結語

什麼是執行緒池

執行緒池,顧名思義就是裝執行緒的池子。其用途是為了幫我們重複管理執行緒,避免建立大量的執行緒增加開銷,提高響應速度。

別再說你不懂執行緒池——做個優雅的攻城獅

為什麼要用執行緒池

作為一個嚴謹的攻城獅,不會希望別人看到我們的程式碼就開始吐槽,new Thread().start()會讓程式碼看起來混亂臃腫,並且不好管理和維護,那麼我們就需要用到了執行緒池。

在程式設計中經常會使用執行緒來非同步處理任務,但是每個執行緒的建立和銷燬都需要一定的開銷。如果每次執行一個任務都需要開一個新執行緒去執行,則這些執行緒的建立和銷燬將消耗大量的資源;並且執行緒都是“各自為政”的,很難對其進行控制,更何況有一堆的執行緒在執行。執行緒池為我們做的,就是執行緒建立之後為我們保留,當我們需要的時候直接拿來用,省去了重複建立銷燬的過程。

執行緒池的處理邏輯

執行緒池ThreadPoolExecutor建構函式

//五個引數的建構函式
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue)

//六個引數的建構函式-1
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory)

//六個引數的建構函式-2
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler)

//七個引數的建構函式
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) 
複製程式碼

雖然引數多,只是看著嚇人,其實很好理解,下面會一一解答。

我們拿最多引數的來說:

1.corePoolSize -> 該執行緒池中核心執行緒數最大值

核心執行緒:在建立完執行緒池之後,核心執行緒先不建立,在接到任務之後建立核心執行緒。並且會一直存在於執行緒池中(即使這個執行緒啥都不幹),有任務要執行時,如果核心執行緒沒有被佔用,會優先用核心執行緒執行任務。數量一般情況下設定為CPU核數的二倍即可。

2.maximumPoolSize -> 該執行緒池中執行緒總數最大值

執行緒總數=核心執行緒數+非核心執行緒數

非核心執行緒:簡單理解,即核心執行緒都被佔用,但還有任務要做,就建立非核心執行緒

3.keepAliveTime -> 非核心執行緒閒置超時時長

這個引數可以理解為,任務少,但池中執行緒多,非核心執行緒不能白養著,超過這個時間不工作的就會被幹掉,但是核心執行緒會保留。

4.TimeUnit -> keepAliveTime的單位

TimeUnit是一個列舉型別,其包括:
NANOSECONDS : 1微毫秒 = 1微秒 / 1000
MICROSECONDS : 1微秒 = 1毫秒 / 1000
MILLISECONDS : 1毫秒 = 1秒 /1000
SECONDS : 秒
MINUTES : 分
HOURS : 小時
DAYS : 天

5.BlockingQueue workQueue -> 執行緒池中的任務佇列

預設情況下,任務進來之後先分配給核心執行緒執行,核心執行緒如果都被佔用,並不會立刻開啟非核心執行緒執行任務,而是將任務插入任務佇列等待執行,核心執行緒會從任務佇列取任務來執行,任務佇列可以設定最大值,一旦插入的任務足夠多,達到最大值,才會建立非核心執行緒執行任務。

常見的workQueue有四種:

1.SynchronousQueue:這個佇列接收到任務的時候,會直接提交給執行緒處理,而不保留它,如果所有執行緒都在工作怎麼辦?那就新建一個執行緒來處理這個任務!所以為了保證不出現<執行緒數達到了maximumPoolSize而不能新建執行緒>的錯誤,使用這個型別佇列的時候,maximumPoolSize一般指定成Integer.MAX_VALUE,即無限大

2.LinkedBlockingQueue:這個佇列接收到任務的時候,如果當前已經建立的核心執行緒數小於執行緒池的核心執行緒數上限,則新建執行緒(核心執行緒)處理任務;如果當前已經建立的核心執行緒數等於核心執行緒數上限,則進入佇列等待。由於這個佇列沒有最大值限制,即所有超過核心執行緒數的任務都將被新增到佇列中,這也就導致了maximumPoolSize的設定失效,因為匯流排程數永遠不會超過corePoolSize

3.ArrayBlockingQueue:可以限定佇列的長度,接收到任務的時候,如果沒有達到corePoolSize的值,則新建執行緒(核心執行緒)執行任務,如果達到了,則入隊等候,如果佇列已滿,則新建執行緒(非核心執行緒)執行任務,又如果匯流排程數到了maximumPoolSize,並且佇列也滿了,則發生錯誤,或是執行實現定義好的飽和策略

4.DelayQueue:佇列內元素必須實現Delayed介面,這就意味著你傳進去的任務必須先實現Delayed介面。這個佇列接收到任務時,首先先入隊,只有達到了指定的延時時間,才會執行任務

6.ThreadFactory threadFactory -> 建立執行緒的工廠

可以用執行緒工廠給每個建立出來的執行緒設定名字。一般情況下無須設定該引數。

7.RejectedExecutionHandler handler -> 飽和策略

這是當任務佇列和執行緒池都滿了時所採取的應對策略,預設是AbordPolicy, 表示無法處理新任務,並丟擲 RejectedExecutionException 異常。此外還有3種策略,它們分別如下。
(1)CallerRunsPolicy:用呼叫者所在的執行緒來處理任務。此策略提供簡單的反饋控制機制,能夠減緩新任務的提交速度。
(2)DiscardPolicy:不能執行的任務,並將該任務刪除。
(3)DiscardOldestPolicy:丟棄佇列最近的任務,並執行當前的任務。

別暈,接下來上圖,相信結合圖你能大徹大悟~

別再說你不懂執行緒池——做個優雅的攻城獅

如何使用執行緒池

說了半天原理,接下來就要用了,java為我們提供了4種執行緒池FixedThreadPoolCachedThreadPoolSingleThreadExecutorScheduledThreadPool,幾乎可以滿足我們大部分的需要了:

1.FixedThreadPool

可重用固定執行緒數的執行緒池,超出的執行緒會在佇列中等待,在Executors類中我們可以找到建立方式:

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

FixedThreadPoolcorePoolSizemaximumPoolSize都設定為引數nThreads,也就是隻有固定數量的核心執行緒,不存在非核心執行緒。keepAliveTime為0L表示多餘的執行緒立刻終止,因為不會產生多餘的執行緒,所以這個引數是無效的。FixedThreadPool的任務佇列採用的是LinkedBlockingQueue。

別再說你不懂執行緒池——做個優雅的攻城獅
建立執行緒池的方法,在我們的程式中只需要,後面其他種類的同理:

public static void main(String[] args) {
        // 引數是要執行緒池的執行緒最大值
        ExecutorService executorService = Executors.newFixedThreadPool(10);
}
複製程式碼

2.CachedThreadPool

CachedThreadPool是一個根據需要建立執行緒的執行緒池

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

CachedThreadPoolcorePoolSize是0,maximumPoolSize是Int的最大值,也就是說CachedThreadPool沒有核心執行緒,全部都是非核心執行緒,並且沒有上限。keepAliveTime是60秒,就是說空閒執行緒等待新任務60秒,超時則銷燬。此處用到的佇列是阻塞佇列SynchronousQueue,這個佇列沒有緩衝區,所以其中最多隻能存在一個元素,有新的任務則阻塞等待。

別再說你不懂執行緒池——做個優雅的攻城獅

3.SingleThreadExecutor

SingleThreadExecutor是使用單個執行緒工作的執行緒池。其建立原始碼如下:

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

我們可以看到匯流排程數和核心執行緒數都是1,所以就只有一個核心執行緒。該執行緒池才用連結串列阻塞佇列LinkedBlockingQueue,先進先出原則,所以保證了任務的按順序逐一進行。

別再說你不懂執行緒池——做個優雅的攻城獅

4.ScheduledThreadPool

ScheduledThreadPool是一個能實現定時和週期性任務的執行緒池,它的建立原始碼如下:

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
複製程式碼

這裡建立了ScheduledThreadPoolExecutor,繼承自ThreadPoolExecutor,主要用於定時延時或者定期處理任務。ScheduledThreadPoolExecutor的構造如下:

    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE,
              DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
              new DelayedWorkQueue());
    }
複製程式碼

可以看出corePoolSize是傳進來的固定值,maximumPoolSize無限大,因為採用的佇列DelayedWorkQueue是無解的,所以maximumPoolSize引數無效。該執行緒池執行如下:

別再說你不懂執行緒池——做個優雅的攻城獅
當執行scheduleAtFixedRate或者scheduleWithFixedDelay方法時,會向DelayedWorkQueue新增一個實現RunnableScheduledFuture介面的ScheduledFutureTask(任務的包裝類),並會檢查執行的執行緒是否達到corePoolSize。如果沒有則新建執行緒並啟動ScheduledFutureTask,然後去執行任務。如果執行的執行緒達到了corePoolSize時,則將任務新增到DelayedWorkQueue中。DelayedWorkQueue會將任務進行排序,先要執行的任務會放在佇列的前面。在跟此前介紹的執行緒池不同的是,當執行完任務後,會將ScheduledFutureTask中的time變數改為下次要執行的時間並放回到DelayedWorkQueue中。

如何合理配置執行緒池的大小

一般需要根據任務的型別來配置執行緒池大小:
如果是CPU密集型任務,就需要儘量壓榨CPU,參考值可以設為 NCPU+1
如果是IO密集型任務,參考值可以設定為2*NCPU
當然,這只是一個參考值,具體的設定還需要根據實際情況進行調整,比如可以先將執行緒池大小設定為參考值,再觀察任務執行情況和系統負載、資源利用率來進行適當調整。

結語

java為我們提供的執行緒池就介紹到這了,牆裂建議大家還是動手去敲一敲,畢竟實踐過心裡才有底。

相關文章