執行緒池

疏影橫斜水清淺發表於2024-04-22

執行緒池

前言

在執行一個非同步任務或併發任務時,往往是透過直接new Thread()方法來建立新的執行緒,這樣做弊端較多,更好的解決方案是合理地利用執行緒池,執行緒池的優勢很明顯,如下:

  1. 降低系統資源消耗,透過重用已存在的執行緒,降低執行緒建立和銷燬造成的消耗;

  2. 提高系統響應速度,當有任務到達時,無需等待新執行緒的建立便能立即執行;

  3. 方便執行緒併發數的管控,執行緒若是無限制的建立,不僅會額外消耗大量系統資源,更是佔用過多資源而阻塞系統或oom等狀況,從而降低系統的穩定性。執行緒池能有效管控執行緒,統一分配、調優,提供資源使用率;

  4. 更強大的功能,執行緒池提供了定時、定期以及可控執行緒數等功能的執行緒池,使用方便簡單。

通用執行緒工廠:

public static class testThreadPoolFactory implements ThreadFactory {

    private AtomicInteger threadIdx = new AtomicInteger(0);

    private String threadNamePrefix;

    public testThreadPoolFactory(String Prefix) {
        threadNamePrefix = Prefix;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r);
        thread.setName(threadNamePrefix + "-xxljob-" + threadIdx.getAndIncrement());
        return thread;
    }
}

java執行緒池
java透過Executors提供四種執行緒池,分別為:

newCachedThreadPool:
建立一個可快取的無界執行緒池,如果執行緒池長度超過處理需要,可靈活回收空執行緒,若無可回收,則新建執行緒。當執行緒池中的執行緒空閒時間超過60s,則會自動回收該執行緒,當任務超過執行緒池的執行緒數則建立新的執行緒,執行緒池的大小上限為Integer.MAX_VALUE,可看作無限大。

/**
 * 可快取無界執行緒池測試
 * 當執行緒池中的執行緒空閒時間超過60s則會自動回收該執行緒,核心執行緒數為0
 * 當任務超過執行緒池的執行緒數則建立新執行緒。執行緒池的大小上限為Integer.MAX_VALUE,
 * 可看做是無限大。
 */
@Test
public void cacheThreadPoolTest() {
    // 建立可快取的無界執行緒池,可以指定執行緒工廠,也可以不指定執行緒工廠
    ExecutorService executorService = Executors.newCachedThreadPool(new testThreadPoolFactory("cachedThread"));
    for (int i = 0; i < 10; i++) {
        executorService.submit(() -> {
            print("cachedThreadPool");
            System.out.println(Thread.currentThread().getName());
                }
        );
    }
}

newFixedThreadPool:

建立一個指定大小的執行緒池,可控制執行緒的最大併發數,超出的執行緒會在LinkedBlockingQueue阻塞佇列中等待

/**
 * 建立固定執行緒數量的執行緒池測試
 * 建立一個固定大小的執行緒池,該方法可指定執行緒池的固定大小,對於超出的執行緒會在LinkedBlockingQueue佇列中等待
 * 核心執行緒數可以指定,執行緒空閒時間為0
 */
@Test
public void fixedThreadPoolTest() {
    ExecutorService executorService = Executors.newFixedThreadPool(5, new testThreadPoolFactory("fixedThreadPool"));
    for (int i = 0; i < 10; i++) {
        executorService.submit(() -> {
                    print("fixedThreadPool");
                    System.out.println(Thread.currentThread().getName());
                }
        );
    }
}

newScheduledThreadPool:

建立一個定長的執行緒池,可以指定執行緒池核心執行緒數,支援定時及週期性任務的執行

/**
 * 建立定時週期執行的執行緒池測試
 *
 * schedule(Runnable command, long delay, TimeUnit unit),延遲一定時間後執行Runnable任務;
 * schedule(Callable callable, long delay, TimeUnit unit),延遲一定時間後執行Callable任務;
 * scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit),延遲一定時間後,以間隔period時間的頻率週期性地執行任務;
 * scheduleWithFixedDelay(Runnable command, long initialDelay, long delay,TimeUnit unit),與scheduleAtFixedRate()方法很類似,
 * 但是不同的是scheduleWithFixedDelay()方法的週期時間間隔是以上一個任務執行結束到下一個任務開始執行的間隔,而scheduleAtFixedRate()方法的週期時間間隔是以上一個任務開始執行到下一個任務開始執行的間隔,
 * 也就是這一些任務系列的觸發時間都是可預知的。
 * ScheduledExecutorService功能強大,對於定時執行的任務,建議多采用該方法。
 *
 * 作者:張老夢
 * 連結:https://www.jianshu.com/p/9ce35af9100e
 * 來源:簡書
 * 著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。
 */
@Test
public void scheduleThreadPoolTest() {
    // 建立指定核心執行緒數,但最大執行緒數是Integer.MAX_VALUE的可定時執行或週期執行任務的執行緒池
    ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5, new testThreadPoolFactory("scheduledThread"));

    // 定時執行一次的任務,延遲1s後執行
    executorService.schedule(new Runnable() {
        @Override
        public void run() {
            print("scheduleThreadPool");
            System.out.println(Thread.currentThread().getName() + ", delay 1s");
        }
    }, 1, TimeUnit.SECONDS);


    // 週期性地執行任務,延遲2s後,每3s一次地週期性執行任務
    executorService.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + ", every 3s");
        }
    }, 2, 3, TimeUnit.SECONDS);


    executorService.scheduleWithFixedDelay(new Runnable() {
        @Override
        public void run() {
            long start = new Date().getTime();
            System.out.println("scheduleWithFixedDelay 開始執行時間:" +
                    DateFormat.getTimeInstance().format(new Date()));
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            long end = new Date().getTime();
            System.out.println("scheduleWithFixedDelay執行花費時間=" + (end - start) / 1000 + "m");
            System.out.println("scheduleWithFixedDelay執行完成時間:"
                    + DateFormat.getTimeInstance().format(new Date()));
            System.out.println("======================================");
        }
    }, 1, 2, TimeUnit.SECONDS);

}

newSingleThreadExecutor:

建立一個單執行緒化的執行緒池,它只有一個執行緒,用僅有的一個執行緒來執行任務,保證所有的任務按照指定順序(FIFO,LIFO,優先順序)執行,所有的任務都儲存在佇列LinkedBlockingQueue中,等待唯一的單執行緒來執行任務。

/**
 * 建立只有一個執行緒的執行緒池測試
 * 該方法無引數,所有任務都儲存佇列LinkedBlockingQueue中,核心執行緒數為1,執行緒空閒時間為0
 * 等待唯一的單執行緒來執行任務,並保證所有任務按照指定順序(FIFO或優先順序)執行
 */
@Test
public void singleThreadPoolTest() {
    // 建立僅有單個執行緒的執行緒池
    ExecutorService executorService = Executors.newSingleThreadExecutor(new testThreadPoolFactory("singleThreadPool"));
    for (int i = 0; i < 10; i++) {
        executorService.submit(() -> {
                    print("singleThreadPool");
                    System.out.println(Thread.currentThread().getName());
                }
        );
    }

}

方法對比

工廠方法 corePoolSize maximumPoolSize keepAliveTime workQueue
newCachedThreadPool 0 Integer.MAX_VALUE 60s SynchronousQueue
newFixedThreadPool nThreads nThreads 0 LinkedBlockingQueue
newSingleThreadExecutor 1 1 0 LinkedBlockingQueue
newScheduledThreadPool corePoolSize Integer.MAX_VALUE 0 DelayedWorkQueue

其他引數都相同,其中執行緒工廠的預設類為DefaultThreadFactory,執行緒飽和的預設策略為ThreadPoolExecutor.AbortPolicy。

執行緒池原理

Executors類提供4個靜態工廠方法:newCachedThreadPool()、newFixedThreadPool(int)、newSingleThreadExecutor和newScheduledThreadPool(int)。這些方法最終都是透過ThreadPoolExecutor類來完成的,這裡強烈建議大家直接使用Executors類提供的便捷的工廠方法,能完成絕大多數的使用者場景,當需要更細節地調整配置,需要先了解每一項引數的意義。

ThreadPoolExecutor

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

建立執行緒池,在構造一個新的執行緒池時,必須滿足下面的條件:

  1. corePoolSize(執行緒池基本大小)必須大於或等於0;
  2. maximumPoolSize(執行緒池最大大小)必須大於或等於1;
  3. maximumPoolSize必須大於或等於corePoolSize;
  4. keepAliveTime(執行緒存活保持時間)必須大於或等於0;
  5. workQueue(任務佇列)不能為空;
  6. threadFactory(執行緒工廠)不能為空,預設為DefaultThreadFactory類
  7. handler(執行緒飽和策略)不能為空,預設策略為ThreadPoolExecutor.AbortPolicy
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;
}

引數說明

  1. corePoolSize(執行緒池基本大小):當向執行緒池提交一個任務時,若執行緒池已建立的執行緒數小於corePoolSize,即便此時存在空閒執行緒,也會透過建立一個新執行緒來執行該任務,直到已建立的執行緒數大於或等於corePoolSize時,才會根據是否存在空閒執行緒,來決定是否需要建立新的執行緒。除了利用提交新任務來建立和啟動執行緒(按需構造),也可以透過 prestartCoreThread() 或 prestartAllCoreThreads() 方法來提前啟動執行緒池中的基本執行緒。
  2. maximumPoolSize(執行緒池最大大小):執行緒池所允許的最大執行緒個數。當佇列滿了,且已建立的執行緒數小於maximumPoolSize,則執行緒池會建立新的執行緒來執行任務。另外,對於無界佇列,可忽略該引數。
  3. keepAliveTime(執行緒存活保持時間):預設情況下,當執行緒池的執行緒個數多於corePoolSize時,執行緒的空閒時間超過keepAliveTime則會終止。但只要keepAliveTime大於0,allowCoreThreadTimeOut(boolean) 方法也可將此超時策略應用於核心執行緒。另外,也可以使用setKeepAliveTime()動態地更改引數。
  4. unit(存活時間的單位):時間單位,分為7類,從細到粗順序:NANOSECONDS(納秒),MICROSECONDS(微妙),MILLISECONDS(毫秒),SECONDS(秒),MINUTES(分),HOURS(小時),DAYS(天);
  5. workQueue(任務佇列):用於傳輸和儲存等待執行任務的阻塞佇列。可以使用此佇列與執行緒池進行互動:
  6. 如果執行的執行緒數少於 corePoolSize,則 Executor 始終首選新增新的執行緒,而不進行排隊。
  7. 如果執行的執行緒數等於或多於 corePoolSize,則 Executor 始終首選將請求加入佇列,而不新增新的執行緒。
  8. 如果無法將請求加入佇列,則建立新的執行緒,除非建立此執行緒超出 maximumPoolSize,在這種情況下,任務將被拒絕。
  9. threadFactory(執行緒工廠):用於建立新執行緒。由同一個threadFactory建立的執行緒,屬於同一個ThreadGroup,建立的執行緒優先順序都為Thread.NORM_PRIORITY,以及是非守護程序狀態。threadFactory建立的執行緒也是採用new Thread()方式,threadFactory建立的執行緒名都具有統一的風格:pool-m-thread-n(m為執行緒池的編號,n為執行緒池內的執行緒編號);
  10. handler(執行緒飽和策略):當執行緒池和佇列都滿了,則表明該執行緒池已達飽和狀態。
  11. ThreadPoolExecutor.AbortPolicy:處理程式遭到拒絕,則直接丟擲執行時異常 RejectedExecutionException。(預設策略)
  12. ThreadPoolExecutor.CallerRunsPolicy:呼叫者所線上程來執行該任務,此策略提供簡單的反饋控制機制,能夠減緩新任務的提交速度。
  13. ThreadPoolExecutor.DiscardPolicy:無法執行的任務將被刪除。
  14. ThreadPoolExecutor.DiscardOldestPolicy:如果執行程式尚未關閉,則位於工作佇列頭部的任務將被刪除,然後重新嘗試執行任務(如果再次失敗,則重複此過程)。

佇列排隊詳解

  • 直接提交。工作佇列的預設選項是 SynchronousQueue,它將任務直接提交給執行緒而不保持它們。在此,如果不存在可用於立即執行任務的執行緒,則試圖把任務加入佇列將失敗,因此會構造一個新的執行緒。此策略可以避免在處理可能具有內部依賴性的請求集時出現鎖。直接提交通常要求無界 maximumPoolSizes 以避免拒絕新提交的任務。當命令以超過佇列所能處理的平均數連續到達時,此策略允許無界執行緒具有增長的可能性。

  • 無界佇列。使用無界佇列(例如,不具有預定義容量的 LinkedBlockingQueue)將導致在所有 corePoolSize 執行緒都忙時新任務在佇列中等待。這樣,建立的執行緒就不會超過 corePoolSize。(因此,maximumPoolSize 的值也就無效了。)當每個任務完全獨立於其他任務,即任務執行互不影響時,適合於使用無界佇列;例如,在 Web 頁伺服器中。這種排隊可用於處理瞬態突發請求,當命令以超過佇列所能處理的平均數連續到達時,此策略允許無界執行緒具有增長的可能性。

  • 有界佇列。當使用有限的 maximumPoolSizes 時,有界佇列(如 ArrayBlockingQueue)有助於防止資源耗盡,但是可能較難調整和控制。佇列大小和最大池大小可能需要相互折衷:使用大型佇列和小型池可以最大限度地降低 CPU 使用率、作業系統資源和上下文切換開銷,但是可能導致人工降低吞吐量。如果任務頻繁阻塞(例如,如果它們是 I/O 邊界),則系統可能為超過您許可的更多執行緒安排時間。使用小型佇列通常要求較大的池大小,CPU 使用率較高,但是可能遇到不可接受的排程開銷,這樣也會降低吞吐量。

工作佇列BlockingQueue詳解

BlockingQueue的插入/移除/檢查這些方法,對於不能立即滿足但可能在將來某一時刻可以滿足的操作,共有4種不同的處理方式:第一種是丟擲一個異常,第二種是返回一個特殊值(null 或 false,具體取決於操作),第三種是在操作可以成功前,無限期地阻塞當前執行緒,第四種是在放棄前只在給定的最大時間限制內阻塞。如下表格:

操作 丟擲異常 特殊值 阻塞 超時
插入 add(e) offer(e) put(e) offer(e, time, unit)
移除 remove() poll() take() poll(time, unit)
檢查 element() peek() 不可用 不可用

實現BlockingQueue介面的常見類如下:

  • ArrayBlockingQueue:基於陣列的有界阻塞佇列。佇列按FIFO原則對元素進行排序,佇列頭部是在佇列中存活時間最長的元素,隊尾則是存在時間最短的元素。新元素插入到佇列的尾部,佇列獲取操作則是從佇列頭部開始獲得元素。 這是一個典型的“有界快取區”,固定大小的陣列在其中保持生產者插入的元素和使用者提取的元素。一旦建立了這樣的快取區,就不能再增加其容量。試圖向已滿佇列中放入元素會導致操作受阻塞;試圖從空佇列中提取元素將導致類似阻塞。ArrayBlockingQueue構造方法可透過設定fairness引數來選擇是否採用公平策略,公平性通常會降低吞吐量,但也減少了可變性和避免了“不平衡性”,可根據情況來決策。

  • LinkedBlockingQueue:基於連結串列的無界阻塞佇列。與ArrayBlockingQueue一樣採用FIFO原則對元素進行排序。基於連結串列的佇列吞吐量通常要高於基於陣列的佇列。

  • SynchronousQueue:同步的阻塞佇列。其中每個插入操作必須等待另一個執行緒的對應移除操作,等待過程一直處於阻塞狀態,同理,每一個移除操作必須等到另一個執行緒的對應插入操作。SynchronousQueue沒有任何容量。不能在同步佇列上進行 peek,因為僅在試圖要移除元素時,該元素才存在;除非另一個執行緒試圖移除某個元素,否則也不能(使用任何方法)插入元素;也不能迭代佇列,因為其中沒有元素可用於迭代。Executors.newCachedThreadPool使用了該佇列。

  • PriorityBlockingQueue:基於優先順序的無界阻塞佇列。優先順序佇列的元素按照其自然順序進行排序,或者根據構造佇列時提供的 Comparator 進行排序,具體取決於所使用的構造方法。優先順序佇列不允許使用 null 元素。依靠自然順序的優先順序佇列還不允許插入不可比較的物件(這樣做可能導致 ClassCastException)。雖然此佇列邏輯上是無界的,但是資源被耗盡時試圖執行 add 操作也將失敗(導致 OutOfMemoryError)。

執行緒池關閉

呼叫執行緒池的shutdown()或shutdownNow()方法來關閉執行緒池

  • shutdown原理:將執行緒池狀態設定成SHUTDOWN狀態,然後中斷所有沒有正在執行任務的執行緒。
  • shutdownNow原理:將執行緒池的狀態設定成STOP狀態,然後中斷所有任務(包括正在執行的)的執行緒,並返回等待執行任務的列表。

中斷採用interrupt方法,所以無法響應中斷的任務可能永遠無法終止。但呼叫上述的兩個關閉之一,isShutdown()方法返回值為true,當所有任務都已關閉,表示執行緒池關閉完成,則isTerminated()方法返回值為true。當需要立刻中斷所有的執行緒,不一定需要執行完任務,可直接呼叫shutdownNow()方法。
執行緒池流程

在這裡插入圖片描述

  1. 判斷核心執行緒池是否已滿,即已建立執行緒數是否小於corePoolSize?沒滿則建立一個新的工作執行緒來執行任務。已滿則進入下個流程。

  2. 判斷工作佇列是否已滿?沒滿則將新提交的任務新增在工作佇列,等待執行。已滿則進入下個流程。

  3. 判斷整個執行緒池是否已滿,即已建立執行緒數是否小於maximumPoolSize?沒滿則建立一個新的工作執行緒來執行任務,已滿則交給飽和策略來處理這個任務。

合理配置執行緒池

需要針對具體情況而具體處理,不同的任務類別應採用不同規模的執行緒池,任務類別可劃分為CPU密集型任務、IO密集型任務和混合型任務。

  • 對於CPU密集型任務:執行緒池中執行緒個數應儘量少,不應大於CPU核心數;

  • 對於IO密集型任務:由於IO操作速度遠低於CPU速度,那麼在執行這類任務時,CPU絕大多數時間處於空閒狀態,那麼執行緒池可以配置儘量多些的執行緒,以提高CPU利用率;

  • 對於混合型任務:可以拆分為CPU密集型任務和IO密集型任務,當這兩類任務執行時間相差無幾時,透過拆分再執行的吞吐率高於序列執行的吞吐率,但若這兩類任務執行時間有資料級的差距,那麼沒有拆分的意義。

執行緒池監控

利用執行緒池提供的引數進行監控,引數如下:

  • taskCount:執行緒池需要執行的任務數量。

  • completedTaskCount:執行緒池在執行過程中已完成的任務數量,小於或等於taskCount。

  • largestPoolSize:執行緒池曾經建立過的最大執行緒數量,透過這個資料可以知道執行緒池是否滿過。如等於執行緒池的最大大小,則表示執行緒池曾經滿了。

  • getPoolSize:執行緒池的執行緒數量。如果執行緒池不銷燬的話,池裡的執行緒不會自動銷燬,所以這個大小隻增不減。

  • getActiveCount:獲取活動的執行緒數。

透過擴充套件執行緒池進行監控:繼承執行緒池並重寫執行緒池的beforeExecute(),afterExecute()和terminated()方法,可以在任務執行前、後和執行緒池關閉前自定義行為。如監控任務的平均執行時間,最大執行時間和最小執行時間等。

相關文章