ThreadPoolExecutor參數圖解

 

 

爲什麼使用ThreadPoolExecutor

在android開發中經常會使用多線程異步來處理相關任務,而如果用傳統的newThread來創建一個子線程進行處理,會造成一些嚴重的問題:

1:在任務衆多的情況下,系統要爲每一個任務創建一個線程,而任務執行完畢後會銷燬每一個線程,所以會造成線程頻繁地創建與銷燬。

2:多個線程頻繁地創建會佔用大量的資源,並且在資源競爭的時候就容易出現問題,同時這麼多的線程缺乏一個統一的管理,容易造成界面的卡頓。

3:多個線程頻繁地銷燬,會頻繁地調用GC機制,這會使性能降低,又非常耗時。

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.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

參數

  • corePoolSize=> 線程池裏的核心線程數量
  • maximumPoolSize=> 線程池裏允許有的最大線程數量
  • keepAliveTime=> 空閒線程存活時間
  • unit=> keepAliveTime的時間單位,比如分鐘,小時等
  • workQueue=> 緩衝隊列
  • threadFactory=> 線程工廠用來創建新的線程放入線程池
  • handler=> 線程池拒絕任務的處理策略,比如拋出異常等策略

一開始我們看到這些參數內心肯定是拒絕的,看源碼也有些崩潰,那這些參數到底是些什麼意思呢?

或者給一組實際數據:

corePoolSize:1
mamximumPoolSize:3
keepAliveTime:60s
workQueue:ArrayBlockingQueue,有界阻塞隊列,隊列大小是4
handler:默認的策略,拋出來一個ThreadPoolRejectException

這些代表什麼呢?

參數可視化

我們把線程池比作一個花瓶

這個花瓶由 瓶口 、 瓶頸 、 瓶身 三個部分組成。

這三個部分分別對應着線程池的三個參數:maximumPoolSize, workQueue,corePoolSize。

 

改變corePoolSize

改變workQueue

線程池裏的線程,我用一個紅色小球表示,每來一個任務,就會生成一個小球:

 

而核心線程,也就是正在處理中的任務,則用灰色的虛線小球表示 (目前第一版動畫先這樣簡陋點吧......)

 

我們往線程池中增加任務

於是畫風就變成了這樣,“花瓶”有這麼幾個重要的參數:

  • corePoolSize=> 瓶身的容量
  • maximumPoolSize=> 瓶口的容量
  • keepAliveTime=> 紅色小球的存活時間
  • unit=> keepAliveTime的時間單位,比如分鐘,小時等
  • workQueue=> 瓶頸,不同類型的瓶頸容量不同
  • threadFactory=> 你投遞小球進花瓶的小手 (線程工廠)
  • handler=> 線程池拒絕任務的處理策略,比如小球被排出瓶外

如果往這個花瓶裏面放入很多小球時(線程池執行任務);

瓶身 (corePoolSize) 裝不下了, 就會堆積到 瓶頸 (queue) 的位置;

瓶頸還是裝不下, 就會堆積到 瓶口 (maximumPoolSize);

直到最後小球從瓶口溢出。

還記得上面提到的那一組實際參數嗎,代表的花瓶大體上是如下圖這樣的:

那麼參數可視化到底有什麼實際意義呢?

阿里的規範

我們最開始 接觸ThreadPoolExcutor的時候,一般使用Executors去創建線程池,但是阿里開發手冊中對於 Java 線程池的使用規範:

一開始我並不知道爲什麼要去限制這樣使用,這四種線程池爲什麼會導致OOM,

我們看看這四種線程池的具體參數,然後再用花瓶動畫演示一下導致OOM的原因。

線程池FixedThreadPool

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

我們關心的參數如下

corePoolSize:nThreads
mamximumPoolSize:nThreads
workQueue:LinkedBlockingQueue

FixedThreadPool表示的花瓶就是下圖這樣子:

線程池SingleThreadPool:

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

我們關心的參數如下

corePoolSize:1
mamximumPoolSize:1
workQueue:LinkedBlockingQueue

SingleThreadPool表示的花瓶就是下圖這樣子:

雖然兩個線程池的樣子沒什麼差異,但是這裏我們發現了一個問題:

爲什麼 FixedThreadPool 和 SingleThreadPool 的 corePoolSize和mamximumPoolSize 要設計成一樣的?

回答這個問題, 我們應該關注一下線程池的 workQueue 參數。

線程池FixedThreadPool和SingleThreadPool 都用到的阻塞隊列 LinkedBlockingQueue。

 

LinkedBlockingQueue

 

 /**
     * Creates a {@code LinkedBlockingQueue} with a capacity of
     * {@link Integer#MAX_VALUE}.
     */
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

從LinkedBlockingQueue的源碼註釋中我們可以看到, 如果不指定隊列的容量, 那麼默認就是接近無限大的。

 

從動畫可以看出, 花瓶的瓶頸是會無限變長的, 也就是說不管瓶口容量設計得多大, 都是沒有作用的!

所以不管線程池FixedThreadPool和SingleThreadPool 的mamximumPoolSize 等於多少, 都是不生效的!

線程池CachedThreadPool

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

我們關心的參數如下

corePoolSize:0
mamximumPoolSize:Integer.MAX_VALUE
workQueue:SynchronousQueue

表示的花瓶就是下圖這樣子:

 

 

這裏我們由發現了一個問題:

爲什麼CachedThreadPool的mamximumPoolSize要設計成接近無限大的?

回答這個問題, 我們再看一下線程池CachedThreadPool的 workQueue 參數:SynchronousQueue。

SynchronousQueue

來看SynchronousQueue的源碼註釋:

A synchronous queue does not have any internal capacity, not even a capacity of one.

從註釋中我們可以看到, 同步隊列可以認爲是容量爲0。一個不存儲元素的阻塞隊列,每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處於阻塞狀態。

所以如果mamximumPoolSize不設計得很大, 就很容易導致溢出。

但是瓶口設置得太大,堆積的小球太多,又會導致OOM(內存溢出)。

 

 

 

 

線程池ScheduledThreadPool

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

我們關心的參數如下

corePoolSize:corePoolSize
mamximumPoolSize:Integer.MAX_VALUE
workQueue:DelayedWorkQueue

可以看到, 這裏出現了一個新的隊列 workQueue:DelayedWorkQueue

DelayedWorkQueue 是無界隊列, 基於數組實現, 隊列的長度可以擴容到 Integer.MAX_VALUE。

同時ScheduledThreadPool的 mamximumPoolSize 也是接近無限大的。

可以想象得到,ScheduledThreadPool就是史上最強花瓶, 極端情況下長度已經突破天際了!

 

到這裏, 相信大家已經明白, 爲什麼這四種線程會導致OOM了。

線程池的狀態

狀態:

  private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;

線程池狀態含義:

  • RUNNING:接受新任務並且處理阻塞隊列裏的任務;

  • SHUTDOWN:拒絕新任務但是處理阻塞隊列裏的任務;

  • STOP:拒絕新任務並且拋棄阻塞隊列裏的任務,同時會中斷正在處理的任務;

  • TIDYING:所有任務都執行完(包含阻塞隊列裏面任務)當前線程池活動線程爲 0,將要調用 terminated 方法;

  • TERMINATED:終止狀態,terminated方法調用完成以後的狀態。

線程池狀態轉換:

       1.RUNNING -> SHUTDOWN:顯式調用 shutdown() 方法,或者隱式調用了 finalize(),它裏面調用了 shutdown() 方法。

       2.RUNNING or SHUTDOWN -> STOP:顯式調用 shutdownNow() 方法時候。

       3.SHUTDOWN -> TIDYING:當線程池和任務隊列都爲空的時候。

       4.STOP -> TIDYING:當線程池爲空的時候。

       5.TIDYING -> TERMINATED:當 terminated() hook 方法執行完成時候。

 

線程池處理流程和原理

我們先看提交任務的源碼:

 public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

通過上面可視化的展現幾個參數,我們大概瞭解它們的意思

線程池的原理,當提交一個新的任務到線程池時,線程池的處理流程如下:

 

å¨è¿éæå¥å¾çæè¿°

 

執行ThreadPoolExcutor的execute方法,可能會遇到以下情況:

  1. 如果線程池中的線程數未達到核心線程數,則創建核心線程處理任務。
  2. 如果線程數大於或者等於核心線程數,則將任務加入任務隊列中,線程池中的空閒線程會不斷的從任務隊列中取出任務進行處理。
  3. 如果任務隊列滿了,並且線程數沒有達到最大線程數,則創建非核心線程去處理任務。
  4. 如果線程數超過了最大線程數,則執行上面提到的幾種飽和策略。

如何配置線程池:

 

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

 

 

 

 

 

 

 

 

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