Java 線程池詳解

  • 什麼是線程池
  • 爲什麼要使用線程池
  • 線程池的處理邏輯
  • 如何使用線程池
  • 如何合理配置線程池的大小

1.什麼是線程池

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

這裏寫圖片描述

2.爲什麼要用線程池

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

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

3.線程池的處理邏輯

3.1線程池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:丟棄隊列最近的任務,並執行當前的任務。
別暈,接下來上圖,相信結合圖你能大徹大悟~

這裏寫圖片描述

4.如何使用線程池

說了半天原理,接下來就要用了,java爲我們提供了4種線程池FixedThreadPool、CachedThreadPool、SingleThreadExecutor、ScheduledThreadPool,幾乎可以滿足我們大部分的需要了:

4.1 FixedThreadPool

可重用固定線程數的線程池,超出的線程會在隊列中等待,在Executors類中我們可以找到創建方式:

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

FixedThreadPool的corePoolSize和maximumPoolSize都設置爲參數nThreads,也就是隻有固定數量的核心線程,不存在非核心線程。keepAliveTime爲0L表示多餘的線程立刻終止,因爲不會產生多餘的線程,所以這個參數是無效的。FixedThreadPool的任務隊列採用的是LinkedBlockingQueue。

這裏寫圖片描述

創建線程池的方法,在我們的程序中只需要,後面其他種類的同理:

public static void main(String[] args) {
        // 參數是要線程池的線程最大值
        ExecutorService executorService = Executors.newFixedThreadPool(10);
}

4.2 CachedThreadPool

CachedThreadPool是一個根據需要創建線程的線程池

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

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

這裏寫圖片描述

4.3 SingleThreadExecutor

SingleThreadExecutor是使用單個線程工作的線程池。其創建源碼如下:

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

我們可以看到總線程數和核心線程數都是1,所以就只有一個核心線程。該線程池才用鏈表阻塞隊列LinkedBlockingQueue,先進先出原則,所以保證了任務的按順序逐一進行。

這裏寫圖片描述

4.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中。

5.如何合理配置線程池的大小

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

轉自:別再說你不懂線程池——做個優雅的攻城獅

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章