1. 程式人生 > 實用技巧 >執行緒池原理

執行緒池原理

1. 為什麼要使用執行緒池

執行緒是很佔用系統資源的,對執行緒管理不善很容易導致系統問題。因此,我們最好使用執行緒池來管理執行緒,使用執行緒池主要有如下好處:

  • 降低資源消耗。通過複用執行緒可以降低執行緒關閉的次數,從而儘可能降低系統性能損耗;
  • 提升系統響應速度。通過複用執行緒,省去建立執行緒的過程,從而提升系統的響應速度;
  • 提高執行緒可管理性。執行緒是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統的穩定性,因此,需要使用執行緒池來管理執行緒。

2. 執行緒池的工作原理

當一個併發任務提交給執行緒池,執行緒池分配執行緒去執行任務的過程如圖所示:

可以看出,執行緒池執行所提交的任務過程主要有以下階段:

  1. 判斷當前執行緒數是否大於了核心執行緒數。如果不是,即使有空閒的執行緒,也會建立一個新執行緒執行剛提交的任務,否則,說明核心執行緒池中所有的執行緒都在執行任務,進入第2步;
  2. 判斷當前阻塞佇列是否已滿,如果未滿,則將任務放進阻塞佇列中;否則,則進入第3步;
  3. 判斷當前的執行緒數是都大於了最大執行緒數,如果沒有,則建立一個新的執行緒來執行任務,否則,根據設定的拒絕策略拒絕任務。

3. 執行緒池的建立

建立執行緒池主要是ThreadPoolExecutor類來完成,(在阿里巴巴開發規範中,不建議使用Executors.new....ThreadPool'的方法來建立執行緒池,因為java提供的執行緒池各自有各自的優缺點,可能並不適合你自己的業務場景,所以最好自己通過

ThreadPoolExecutor配置引數)。ThreadPoolExecutor有許多過載的構造方法,ThreadPoolExecutor的構造方法為:

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

下面對引數進行說明:

corePoolSize:核心執行緒數。當提交一個任務時,如果當前執行緒池中的執行緒數沒有達到corePoolSize,則會建立新的執行緒來執行所提交的任務,即使當前執行緒池有空閒的執行緒。如果當前執行緒個數已經達到了corePoolSize,則不再重新建立執行緒。如果呼叫了prestartCoreThread()或者 prestartAllCoreThreads(),執行緒池建立的時候所有的核心執行緒都會被建立並且啟動。

maximumPoolSize:最大執行緒個數。如果當阻塞佇列已滿時,並且當前執行緒池執行緒數沒有超過maximumPoolSize的話,就會建立新的執行緒來執行任務。

keepAliveTime:空閒執行緒存活時間。如果當前執行緒池的執行緒數超過了corePoolSize,並且執行緒空閒時間超過了keepAliveTime的話,就會將這些空閒執行緒銷燬,這樣可以儘可能降低系統資源消耗。

unit:時間單位。為keepAliveTime指定時間單位。

workQueue:阻塞佇列。儲存任務的阻塞佇列,關於阻塞佇列可以看這篇文章

threadFactory:執行緒工廠。可以通過指定執行緒工廠為每個創建出來的執行緒設定名字,如果出現併發問題,也方便定位問題。

handler:拒絕策略。當執行緒池的阻塞佇列已滿和執行緒數達到了最大執行緒數,說明當前執行緒池已經處於飽和狀態了,那麼就需要採用一種策略來處理這種情況。採用的策略有這幾種:

  • AbortPolicy: 直接拒絕所提交的任務,丟擲RejectedExecutionException異常;
  • CallerRunsPolicy:用呼叫者所在的執行緒來執行任務;
  • DiscardPolicy:不處理直接丟棄掉任務;
  • DiscardOldestPolicy:丟棄掉阻塞佇列中存放時間最久的任務,執行當前任務。

4.執行緒池執行邏輯

通過ThreadPoolExecutor建立執行緒池後,提交任務後執行過程是怎樣的,下面通過原始碼來看一下。execute方法原始碼如下:

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();
    //如果執行緒池的執行緒個數少於corePoolSize則建立新執行緒執行當前任務
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    //如果執行緒個數大於corePoolSize或者建立執行緒失敗,則將任務存放在阻塞佇列workQueue中
    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);
}

execute方法執行邏輯有這樣幾種情況(與上文提到的過程基本一致,這裡再說一下):

  1. 如果當前執行緒數少於corePoolSize,則會建立新的執行緒來執行新的任務;
  2. 如果當前執行緒數等於或者大於corePoolSize,則會將任務存放到阻塞佇列中;
  3. 如果阻塞佇列已滿,則會建立新的執行緒來執行任務;
  4. 如果當前執行緒數超過了maximumPoolSize,則會使拒絕策略來拒絕任務。

5. 執行緒池的關閉

可以通過shutdown和shutdownNow這兩個方法來關閉執行緒池。他們都是遍歷執行緒池中所有的執行緒,然後依次中斷執行緒。

shutdown和shutdownNow的區別是:

  • shutdownNow首先將執行緒池的狀態設定為STOP,然後嘗試停止所有的正在執行和未執行任務的執行緒,並返回等待執行任務的列表;
  • shutdown是將執行緒池的狀態設定為SHUTDOWN狀態,然後中斷所有沒有正在執行任務的執行緒

可以看出shutdown方法會將正在執行的任務繼續執行完,而shutdownNow會直接中斷正在執行的任務。當我們呼叫了其中任意一個方法,isShutdown方法都會返回true,但是隻有當所有的執行緒都關閉成功,才表示執行緒池成功關閉,這時呼叫isTerminated方法才會返回true。

6. 如何合理配置執行緒池引數

要想合理的配置執行緒池引數,就必須先分析任務特性,可以從以下幾個角度來分析:

  • 任務的性質:CPU密集型任務,IO密集型任務和混合型任務。
  • 任務的優先順序:高,中和低。
  • 任務的執行時間:長,中和短。
  • 任務的依賴性:是否依賴其他系統資源,如資料庫連線。

1.任務性質不同的任務可以用不同規模的執行緒池分開處理。對於CPU密集型任務,配置儘可能少的執行緒數量,一般配置Ncpu+1個執行緒大小的執行緒池。對於IO密集型任務,由於需要等待IO操作,執行緒並不是一直在執行任務,則配置儘可能多的執行緒,如2xNcpu對於混合型的任務,如果可以拆分,則將其拆分成一個CPU密集型任務和IO密集型任務,只要這兩個任務執行的時間相差不是太大,那麼分解後執行的吞吐率要高於序列執行的吞吐率,如果這兩個任務執行時間相差太大,則沒必要進行分解。我們可以通過Runtime.getRuntime().availableProcessors()方法獲得當前裝置的CPU個數

2.優先順序不同的任務可以使用優先順序佇列PriorityBlockingQueue來處理。它可以讓優先順序高的任務先得到執行,需要注意的是如果一直有優先順序高的任務提交到佇列裡,那麼優先順序低的任務可能永遠不能執行。

3.執行時間不同的任務可以交給不同規模的執行緒池來處理,或者也可以使用優先順序佇列,讓執行時間短的任務先執行

4.依賴資料庫連線池的任務,因為執行緒提交SQL後需要等待資料庫返回結果,如果等待的時間越長CPU空閒時間就越長,那麼執行緒數應該設定越大,這樣才能更好的利用CPU。

並且,阻塞佇列最好是使用有界佇列,如果採用無界佇列的話,一旦任務積壓在阻塞佇列中的話就會佔用過多的記憶體資源,甚至會使得系統崩潰。

最後,推薦一本書籍《Java併發程式設計的藝術》