Java四種線程池原理&應用分析

線程池種類

線程池的實現分爲四類:

Executors.newCachedThreadPool()
Executors.newFixedThreadPool(int size)
Executors.newScheduledThreadPool(int size)
Executors.newSingleThreadExecutor()

ThreadPoolExecutor構造參數

四個線程池類都是通過Executors工廠來實現的,通過內部代碼不難發現它們內部構造都依賴於ThreadPoolExecutor實現的,只是不同類型的線程池通過不同的構造參數構建了不同的ThreadPoolExecutor來實現,所以我們這裏先來了解一下構造線程池時對應的核心構造參數。
參數最全的構造函數爲:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler){
...
}

對應參數作用如下:

  • int corePoolSize:線程池中維護的核心線程數數量,這裏維護的線程是即便線程空閒狀態也會保持持有不釋放的。
  • int maximumPoolSize:線程池最多能夠持有的線程數,如果任務過多,線程數會超過corePoolSize,此時會繼續創建新的線程來處理任務,但是最終線程數不能超過maximumPoolSize
  • long keepAliveTime:當線程數超過corePoolSize時,超出部分線程在任務執行完畢後不會馬上釋放,而是會進入等待,但是不會一直等待,當等待時間超過keepAliveTime後會回收對應線程。
  • TimeUnit unit: 這個很簡單,就是keepAliveTime的單位
  • BlockingQueue<Runnable> workQueue:這是一個隊列,當外部調用線程池的execute方法請求線程池執行對應任務的時候,可能不一定能夠馬上獲取的執行所需的線程,那麼此時會先將這些提交過來的任務放到這個workQueue中,當後續有線程資源空出來後再從隊列中取出等待的任務執行。
  • ThreadFactory threadFactory: 這個是一個線程創建的工廠類,通過它對外開放線程創建的邏輯,因爲可能某些任務需要創建一些封裝的特殊線程,此時可以通過自定義一個線程創建工廠,傳入線程池創建構造函數中。
  • RejectedExecutionHandler handler: 這個是設置拒絕策略的,因爲有可能我們傳入的隊列設置了隊列的最大長度或者是提交任務的時候還沒來得及啓動線程池已經要改變狀態爲SHUTDOWN/STOP,因爲以上原因導致任務提交失敗,此時就會調用我們設置好的拒絕策略來進行處理。

瞭解了這些構造參數作用後,我們再來看四種線程池的構造方式和分別對應的應用場景。

newCachedThreadPool()

內部構造函數如下:

new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>())

可以看到構造函數中核心線程數爲0,線程最大數爲Integer.MAX_VALUE,線程回收閥值爲60s,採用的是SynchronousQueue這個同步隊列。那麼這種類型的意思就是說,默認沒任務的時候是不會有核心線程的,只有有任務進來之後纔會創建線程執行任務,然後任務執行完畢之後60s內如果有其它任務提交,則直接用還沒回收的空閒線程來執行,這種模式比較適合的場景是:很多短期異步任務的任務需要提交到線程池執行的情況。

newFixedThreadPool()

構造函數如下:

new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());

可以看到其中corePoolSizemaximumPoolSize大小一樣,並且keepAliveTime=0意思就是不過期,說明構造的是一個定長的線程池,線程創建後會一直存在,這樣的好處是可以很好的控制資源使用,適用於負載比較高的場景。

newScheduledThreadPool()

構造方法如下:

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS,
              new DelayedWorkQueue());
    }

首先,這種線程池中的線程也是創建後一直存在的,比較特殊的是它的隊列是一個DelayedWorkQueue隊列,這個隊列的作用是可以對任務進行延遲執行任務。

newSingleThreadExecutor()

依據名稱也可以很好理解這個了,構造方法如下:

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

可以看到這是一個長度爲1的固定線程池,緩存任務的隊列是LinkedBlockingQueue,意味着可以無限制的追加等待的任務,由於線程池內部只有一個線程,所以可以理解爲任務執行是依據提交順序順序執行的,可以保證任務執行順序,所以對於後臺需要單個線程執行並且保證提交任務順序性的場景,比較適合使用這種線程池。

線程池配置

某些情況下我們可能需要自己直接調用ThreadPoolExecutor來自己定義對應的各項參數,或者是在用Executors工廠創建線程池時要我們自己來指定線程數,這裏關於線程數的設置是對線程池性能有很大影響的,所以我們如何設置線程數就十分重要了。設置線程數量需要考慮我們要計劃要提交到線程池中的任務的類型來考慮,主要爲:

  • CPU密集型任務:指的是任務要執行很多計算類型的操作,十分消耗CPU,例如視頻解碼、複雜運算等,此類任務如果線程數過多會導致頻繁的CPU任務上下文切換,所以此時線程池線程數最好和CPU核心數相近,例如直接設置爲CPU核心數個數的線程數。
  • I/O密集型任務:I/O密集型任務就很好理解了,就是任務可能會頻繁的需要進行網絡、磁盤等IO,此類任務在等待IO的時候CPU消耗很少,這類任務就可以多設置一些線程數,例如可以設置爲CPU核心數的兩倍的線程數。

參考:https://www.jianshu.com/p/1f5195dcc75b

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