執行緒池核心原理淺析

fuxing.發表於2024-05-08

前言

由於系統資源是有限的,為了降低資源消耗,提高系統的效能和穩定性,引入了執行緒池對執行緒進行統一的管理和監控,本文將詳細講解執行緒池的使用、原理。


為什麼使用執行緒池

池化思想

執行緒池主要用到了池化思想,池化思想在計算機領域十分常見,主要用於減少資源浪費、提高效能等。

池化思想主要包含以下幾個方面:

fuxing

一些常見的資源池包括執行緒池、資料庫連線池、物件池、快取池、連線池等。

池化思想可以提高系統的效能,因為它減少了資源的建立和銷燬次數,避免了不必要的開銷。透過池化,系統可以更好地應對高併發情況,降低資源競爭,提高響應速度。

什麼是執行緒池

根據池化思想,在一個系統中,為了避免執行緒頻繁的建立和銷燬,讓執行緒可以複用,引入了執行緒池的概念。執行緒池中,總有那麼幾個活躍執行緒。

當你需要使用執行緒時,可以從池子中隨便拿一個空閒執行緒,當完成工作時,並不急著關閉執行緒,而是將這個執行緒退回到池子,方便其他人使用。

簡單說就是,在使用執行緒池後,建立執行緒變成了從執行緒池中獲得空閒執行緒,關閉執行緒程式設計了向池子裡歸還執行緒。

大致流程如下:

fuxing
## 為什麼使用執行緒池 Java 中的執行緒池是運用場景最多的併發框架,幾乎所有需要非同步或併發執行任務的程式都可以使用執行緒池。

在開發過程中,合理地使用執行緒池能夠帶來3個好處。

  1. 降低資源消耗。透過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗。
  2. 提高響應速度。當任務到達時,任務可以不需要等到執行緒建立就能立即執行。
  3. 提高執行緒的可管理性。執行緒是稀缺資源,如果無限制地建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一分配、調優和監控。

要做到合理利用執行緒池,必須對其實現原理了如指掌。

執行緒池的使用

fuxing
## ThreadPoolExecutor ThreadPoolExecutor 的建立方法總體來說可分為 2 種:
  • 透過 ThreadPoolExecutor 建構函式
  • 透過 Executors 類建立

透過建構函式

1.1. 入參含義

這個也是推薦使用的方法,因為透過 Executors 類建立可能會導致 OOM,如下圖阿里開發規範中的描述。

fuxing

建構函式入參:

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

建構函式入參含義:

fuxing

1.2. 阻塞佇列

workQueue 可選的 BlockingQueue:

fuxing

1.3. 拒絕策略

fuxing

如下圖,上述拒絕策略均實現 RejectedExecutionHandler 介面,且為 ThreadPoolExecutor 的內部類。

fuxing

若以上策略仍無法滿足實際應用需要,完全可以自已擴充套件 RejectedExecutionHandler 介面。

public interface RejectedExecutionHandler {

    /**
     * @param r 當前請求執行的任務
     * @param executor 當前的執行緒池
     */
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

示例:

public class RejectedExecutionDemo {
    public static class MyTask implements Runnable{

        @Override
        public void run() {
            System.out.println(new Date() + ":Thread ID is" + Thread.currentThread().getId());

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyTask myTask = new MyTask();
        ExecutorService executorService = new ThreadPoolExecutor(5, 5,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(10),
                Executors.defaultThreadFactory(),
                (r, executor) -> System.out.println(r.hashCode() + "is discard")
        );

        for (int i = 0; i < 100; i++) {
            executorService.submit(myTask);
            Thread.sleep(10);
        }
    }
}

上述示例中,mytask 執行需要花費100毫秒,因此,必然會導致一些任務被直接丟棄。在實際應用中,我們可以將更詳細的資訊記錄到日誌中,來分析任務丟失情況和系統負載。

fuxing

透過 Executors

Executors 類扮演著執行緒池工廠的角色,透過該類可以取得一個擁有定功能的執行緒池。

該類可以建立三種型別的 ThreadPoolExecutor:

  • FixedThreadPool
  • SingleThreadExecutor
  • CachedThreadPool

2.1. FixedThreadPool

固定執行緒數的執行緒池,該執行緒池中的執行緒數量始終不變。當有一個新的任務提交時,執行緒池中若有空閒執行緒,則立即執行。若沒有,則新的任務會被暫時存在任務佇列中,待有執行緒空閒時,在處理佇列中的任務。

FixedThreadPool 使用的無界任務佇列 LinkedBlockingQueue,可能造成記憶體洩露。

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

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

2.2. SingleThreadExecutor

只有一個工作執行緒的執行緒池,當多於 1 個任務被提交時,會存到任務佇列中。該執行緒池使用的無界任務佇列 LinkedBlockingQueue,可能造成記憶體洩露。

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

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

2.3. CachedThreadPool

根據實際情況調整執行緒數的執行緒池,執行緒池的執行緒數量不確定,若有空閒執行緒可複用,則會優先使用。若所有執行緒均在工作,此時新的任務則會建立新的執行緒優先處理。所有執行緒在任務執行完畢後,將返回執行緒池進行復用。

corePoolSize 被設定為0,maximumPoolSize 被設定為無界,存活時間設定為 60s,空閒執行緒超過60秒後將會被
終止。極端情況執行緒建立過多,會導致記憶體洩露。

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


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

ScheduledThreadPoolExecutor

簡介

如下圖, ScheduledThreadPoolExecutor 繼承自ThreadPoolExecutor,它主要用來定期執行任務,功能與 Timer 類似且更加強大,可以在建構函式中指定多個對應的後臺執行緒數。

fuxing

使用

可透過 Executors 建立,原始碼如下:

public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) {
    return new DelegatedScheduledExecutorService
        (new ScheduledThreadPoolExecutor(1, threadFactory));
}

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

public static ScheduledExecutorService newScheduledThreadPool(
        int corePoolSize, ThreadFactory threadFactory) {
    return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}

這裡的返回值是 ScheduledExecutorService,根據時間對執行緒進行排程。有三個主要方法:

public interface ScheduledExecutorService extends ExecutorService {

    /**
     * 給定時間對任務進行排程
     */
    public ScheduledFuture<?> schedule(Runnable command,
                                       long delay, TimeUnit unit);

    /**
     * 週期性對任務進行排程
     * 以第一個任務的開始時間 initialDelay + period 
     * 第一個任務在 initialDelay + period 執行
     * 第二個任務在 initialDelay + period * 2 執行
     */
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit);

    /**
     * 週期性對任務進行排程
     * 上一個任務結束後,再經過 period 時間開始執行
     */
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);
}

如果任務遇到異常,那麼後續的所有子任務都會停止排程,因此,必須保證異常被及時處理,為週期性任務的穩定排程提供條件。

ForkJoinPool

fork 是開啟子程序,join 是等待,意思是分支子程序結束後才能得到結果,實際開發中,若頻繁的 fork 開啟執行緒可能嚴重影響系統效能,所以引入了 ForkJoinPool。

大致流程是,向 ForkJoinPool 執行緒池中提交一個 ForkJoinTask 任務,就是將任務分解成多個小任務,等任務全部完成後進行處理,這裡採用了分治的思想,具體我將在後續單獨展開,這裡不多做贅述。

ForkJoin 可能出現兩個問題:

  1. 子執行緒積累過多,可能導致系統效能嚴重下降;
  2. 呼叫層次過深,可能導致棧溢位。

執行緒池的任務提交

execute()

該方法用於提交不需要返回值的任務,且無法判斷任務是否被執行緒池執行成功。

原始碼見下面的執行緒池原理章節。

submit()

該方法用於提交需要返回值的任務。執行緒池會返回 Future 物件,可以判斷任務是否執行成功,還可以透過 Future 的get()方法來獲取返回值。

get()方法會阻塞當前執行緒直到任務完成,還可以設定超時時間,到時立即返回,不過這時有可能任務沒有執行完。

public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}

執行緒池的關閉

可以透過呼叫執行緒池的 shutdown 或 shutdownNow 方法來關閉執行緒池。

它們的原理是遍歷執行緒池中的工作執行緒,然後逐個呼叫執行緒的 interrupt() 來中斷執行緒,所以無法響應中斷的任務可能永遠無法終止。

兩種方法存在一定的區別,shutdownNow首先將執行緒池的狀態設定成 STOP,然後嘗試停止所有的正在執行或暫停任務的執行緒,並返回等待執行任務的列表。而 shutdown 只是將執行緒池的狀態設定成 SHUTDOWN 狀態,然後中斷所有沒有正在執行任務的執行緒。

只要呼叫了這兩個關閉方法中的任意一個,isShutdown方法就會返回true。當所有的任務都已關閉後,表示執行緒池關閉成功,這時呼叫isTerminaed方法會返回true。

至於應該呼叫哪一種方法來關閉執行緒池,應該由提交到執行緒池的任務特性決定,通常呼叫 shutdown 方法來關閉執行緒池,如果任務不一定要執行完,則可以呼叫 shutdownNow 方法。

執行緒池執行原理

執行原始碼

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();

    int c = ctl.get();
    
    // 如果當前工作執行緒數是否小於核心執行緒數
    if (workerCountOf(c) < corePoolSize) {
        // 新增核心執行緒去執行任務,成功則return
        if (addWorker(command, true))
            return;
        // 新增失敗,ctl有變化,需重新獲取
        c = ctl.get();
    }


    // 判斷是否為RUNNING,此時核心執行緒數已滿,需加入任務佇列
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 檢查若不是RUNNING則將任務從佇列移除
        if (! isRunning(recheck) && remove(command))
            // 執行拒絕策略
            reject(command);
            
        // 正常則新增一個非核心空執行緒,執行佇列中的任務
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }

    // 表示核心執行緒滿了,佇列也滿了,建立非核心執行緒,執行任務
    else if (!addWorker(command, false))
        // 最大執行緒數也滿了,走拒絕策略
        reject(command);
}

流程圖

fuxing

參考:
[1] 魏鵬. Java併發程式設計的藝術.
[2] 葛一鳴/郭超. 實戰Java高併發程式設計.

相關文章