線程池 是啥?

一、爲什麼要用線程池

一個線程執行某個任務的時間片可以分成3塊,創建線程T1,執行任務T2,銷燬線程T3,現實中往往是 T1+T3>T2,也就是真正執行任務花費的時間很短,反而是創建與銷燬線程更耗時,也是就是若我們需要多個線程去執行任務時,線程的創建和銷燬會佔用更多資源。於是有人提出提前創建一堆線程,然後把它們放在一個容器中統一進行管理,需要用的時候就直接拿出來用,用完之後再放回池子裏。這樣就不會在線程的創建和銷燬上浪費時間。

上面的“池子”就是線程池,很明顯線程池可以給我們帶來很多好處:

  • 低資源消耗,降低了頻繁創建線程和銷燬線程的開銷
  • 提高響應速度
  • 提高線程的可管理性,可以對線程進行一些操作,方便管理線程

二、線程池中的核心參數 

這個池子給我們帶來很多好處,但這個池子不是沒有邊界的,需要一些參數來限制這個池子。

2.1  corePoolSize

核心線程的最大個數,這個參數跟後面講述的線程池的實現原理有非常大的關係。在創建了線程池後,默認情況下,線程池中並沒有任何線程,而是等待有任務到來才創建線程去執行任務,除非調用了prestartAllCoreThreads()或者prestartCoreThread()方法,從這2個方法的名字就可以看出,是預創建線程的意思,即在沒有任務到來之前就創建corePoolSize個線程或者一個線程。默認情況下,在創建了線程池後,線程池中的線程數爲0,當有任務來之後,就會創建一個線程去執行任務,當線程池中的線程數目達到corePoolSize後,就會把到達的任務放到緩存隊列當中。

非核心線程:當等待隊列滿了,如果當前線程數沒有超過最大線程數,則會新建線程執行任務,那麼核心線程和非核心線程到底有什麼區別呢?說出來你可能不信,本質上它們沒有什麼區別,創建出來的線程也根本沒有標識去區分它們是核心還是非核心的,線程池只會去判斷已有的線程數(包括核心和非核心)去跟核心線程數和最大線程數比較,來決定下一步的策略。

2.2  maximumPoolSize

線程池最大線程數,它表示在線程池中最多能創建多少個線程。線程數量超過這個值就會拋異常。

 

2.3  keepAliveTime

表示線程沒有任務執行時最多保持多久時間會終止。默認情況下,只有當線程池中的線程數大於corePoolSize時,keepAliveTime纔會起作用,直到線程池中的線程數不大於corePoolSize,即當線程池中的線程數大於corePoolSize時,如果一個線程空閒的時間達到keepAliveTime,則會終止,直到線程池中的線程數不超過corePoolSize。但是allowCoreThreadTimeOut(true)方法可以使得線程池中的線程數不大於corePoolSize時,keepAliveTime參數也會起作用,直到線程池中的線程數爲0。

unit是參數keepAliveTime的時間單位,有7種取值,在TimeUnit類中有7種靜態屬性:

TimeUnit.DAYS;               //天
TimeUnit.HOURS;             //小時
TimeUnit.MINUTES;           //分鐘
TimeUnit.SECONDS;           //秒
TimeUnit.MILLISECONDS;      //毫秒
TimeUnit.MICROSECONDS;      //微妙
TimeUnit.NANOSECONDS;       //納秒

2.4  workQueue

任務隊列,是一個阻塞隊列,用來存儲等待執行的任務,會對線程池的運行過程產生重大影響,一般來說,這裏的阻塞隊列有以下幾種選擇:ArrayBlockingQueue;LinkedBlockingQueue;SynchronousQueue。

  1. ArrayBlockingQueue: 這是一個由數組實現的容量固定的有界阻塞隊列,,此隊列按 FIFO(先進先出)原則對元素進行排序
  2. SynchronousQueue: 沒有容量,不能緩存數據;每個put必須等待一個take; offer()的時候如果沒有另一個線程在poll()或者take()的話返回false。靜態工廠方法Executors.newCachedThreadPool使用了這個隊列。
  3. LinkedBlockingQueue: 這是一個由單鏈表實現的默認無界的阻塞隊列。LinkedBlockingQueue提供了一個可選有界的構造函數,而在未指明容量時,容量默認爲Integer.MAX_VALUE。靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列。

2.5  threadFactory

線程工廠,主要用來創建線程。

2.6  handler

表示當拒絕處理任務時的策略,有以下四種取值:

ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。 
ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。 
ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新嘗試執行任務(重複此過程)
ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務 

三、 Executor、ExecutorService、Executors

 

 

 Executor 是一個抽象層面的核心接口,它定義了execute()方法,用來接收一個Runnable接口的對象。

ExecutorService 接口繼承了Executor 接口,是Executor 的子接口。ExecutorService 接口對 Executor 接口進行了擴展,提供了返回 Future 對象,終止,關閉線程池等方法。當調用 shutDown 方法時,線程池會停止接受新的任務,但會完成正在 pending 中的任務。Executor接口中execute()方法不返回任何結果,而ExecutorService接口中submit()方法可以通過一個 Future 對象返回運算結果。通過 ExecutorService.submit() 方法返回的 Future 對象,還可以取消任務的執行。Future 提供了 cancel() 方法用來取消執行 pending 中的任務。

Executors 類提供了若干個靜態方法,用於生成不同類型的線程池。但線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式,下面將介紹。

 

四、ThreadPoolExecutor

講完了上面的核心參數就可以看看怎麼創建線程池了,ThreadPoolExecutor是線程池的核心類,有4個構造方法可以得到我們需要的線程池:

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
}

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             threadFactory, defaultHandler);
}

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), handler);
 }

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;
    }

五、線程的任務處理流程

當在execute(Runnable)方法中提交新任務並且少於corePoolSize線程正在運行時,即使其他工作線程處於空閒狀態,也會創建一個新線程來處理該請求。 如果有多於corePoolSize但小於maximumPoolSize線程正在運行,則僅當隊列已滿時纔會創建新線程。 通過設置corePoolSize和maximumPoolSize相同,您可以創建一個固定大小的線程池。 通過將maximumPoolSize設置爲基本上無界的值,例如Integer.MAX_VALUE,您可以允許池容納任意數量的併發任務。 通常,核心和最大池大小僅在構建時設置,但也可以使用setCorePoolSize和setMaximumPoolSize進行動態更改。

六、常見的線程池及其使用場景

在 Executors 類裏面提供了一些靜態工廠,生成一些常用的線程池。

6.1  newFixedThreadPool

創建固定大小的線程池,每提交一個任務就是一個線程,直到達到線程池的最大數量,然後後面進入等待隊列直到前面的任務完成才繼續執行。線程池的大小一旦達到最大值就會保持不變,如果某個線程因爲執行異常而結束,那麼線程池會補充一個新線程。

6.2  newCachedThreadPool(推薦使用)

創建一個可緩存的線程池。如果線程池的大小超過了處理任務所需要的線程,那麼就會回收部分空閒(60秒不執行任務)的線程,當任務數增加時,此線程池又可以智能的添加新線程來處理任務。此線程池不會對線程池大小做限制,線程池大小完全依賴於操作系統(或者說 JVM)能夠創建的最大線程大小。

6.3  newSingleThreadExecutor

創建一個單線程的線程池。這個線程池只有一個線程在工作,也就是相當於單線程串行執行所有任務。如果這個唯一的線程因爲異常結束,那麼會有一個新的線程來替代它。此線程池保證所有任務的執行順序按照任務的提交順序執行。

6.4  newScheduledThreadPool

創建一個大小無限的線程池。此線程池支持定時以及週期性執行任務的需求。

 

線程池不允許使用Executors去創建,而是通過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。 說明:Executors各個方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor:
  主要問題是堆積的請求處理隊列可能會耗費非常大的內存,甚至OOM。
2)newCachedThreadPool和newScheduledThreadPool:
  主要問題是線程數最大數是Integer.MAX_VALUE,可能會創建數量非常多的線程,甚至OOM。

 七、如何合理配置線程池的大小

線程池究竟設成多大是要看你給線程池處理什麼樣的任務,任務類型不同,線程池大小的設置方式也是不同的。

任務一般可分爲:CPU密集型、IO密集型、混合型,對於不同類型的任務需要分配不同大小的線程池。

  • CPU密集型任務
    儘量使用較小的線程池,一般爲CPU核心數+1。
    因爲CPU密集型任務使得CPU使用率很高,若開過多的線程數,只能增加上下文切換的次數,因此會帶來額外的開銷。
  • IO密集型任務
    可以使用稍大的線程池,一般爲2*CPU核心數。
    IO密集型任務CPU使用率並不高,因此可以讓CPU在等待IO的時候去處理別的任務,充分利用CPU時間。
  • 混合型任務
    可以將任務分成IO密集型和CPU密集型任務,然後分別用不同的線程池去處理。
    只要分完之後兩個任務的執行時間相差不大,那麼就會比串行執行來的高效。
    因爲如果劃分之後兩個任務執行時間相差甚遠,那麼先執行完的任務就要等後執行完的任務,最終的時間仍然取決於後執行完的任務,而且還要加上任務拆分與合併的開銷,得不償失。

八、怎麼理解無界隊列和有界隊列

有界隊列
1.初始的poolSize < corePoolSize,提交的runnable任務,會直接做爲new一個Thread的參數,立馬執行 。
2.當提交的任務數超過了corePoolSize,會將當前的runable提交到一個block queue中。
3.有界隊列滿了之後,如果poolSize < maximumPoolsize時,會嘗試new 一個Thread的進行救急處理,立馬執行對應的runnable任務。
4.如果3中也無法處理了,就會走到第四步執行reject操作。
無界隊列
與有界隊列相比,除非系統資源耗盡,否則無界的任務隊列不存在任務入隊失敗的情況。當有新的任務到來,系統的線程數小於corePoolSize時,則新建線程執行任務。當達到corePoolSize後,就不會繼續增加,若後續仍有新的任務加入,而沒有空閒的線程資源,則任務直接進入隊列等待。若任務創建和處理的速度差異很大,無界隊列會保持快速增長,直到耗盡系統內存。當線程池的任務緩存隊列已滿並且線程池中的線程數目達到maximumPoolSize,如果還有任務到來就會採取任務拒絕策略。

 

 

 

 

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