java-20:ThreadPoolExecutor線程池概述和基本用法

本文來自:https://blog.csdn.net/wtopps/article/details/80682267

博主:wtopps

------

前言

在互聯網的開發場景下,很多業務場景下我們需要使用到多線程的技術,從 Java 5 開始,Java 提供了自己的線程池,線程池就是一個線程的容器,每次只執行額定數量的線程。java.util.concurrent包中提供了ThreadPoolExecutor類來管理線程,本文將介紹一下ThreadPoolExecutor類的使用。


爲什麼要使用線程池?

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

  • 降低系統資源消耗,通過重用已存在的線程,降低線程創建和銷燬造成的消耗;
  • 提高系統響應速度,當有任務到達時,無需等待新線程的創建便能立即執行;
  • 方便線程併發數的管控,線程若是無限制的創建,不僅會額外消耗大量系統資源,更是佔用過多資源而阻塞系統或oom等狀況,從而降低系統的穩定性。線程池能有效管控線程,統一分配、調優,提供資源使用率;
  • 更強大的功能,線程池提供了定時、定期以及可控線程數等功能的線程池,使用方便簡單。

線程池使用方式

java.util.concurrent包中提供了多種線程池的創建方式,我們可以直接使用ThreadPoolExecutor類直接創建一個線程池,也可以使用Executors類創建,下面我們分別說一下這幾種創建的方式。


Executors創建線程池

Executors類是java.util.concurrent提供的一個創建線程池的工廠類,使用該類可以方便的創建線程池,此類提供的幾種方法,支持創建四種類型的線程池,分別是:newCachedThreadPool、newFixedThreadPool、newScheduledThreadPool、newSingleThreadExecutor。

newCachedThreadPool

創建一個可緩存的無界線程池,該方法無參數。當線程池中的線程空閒時間超過60s則會自動回收該線程,當任務超過線程池的線程數則創建新線程。線程池的大小上限爲Integer.MAX_VALUE,可看做是無限大。

/**
     * 創建無邊界大小的線程池
     */
    public static void createCachedThreadPool() {
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        final CountDownLatch countDownLatch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            final int currentIndex = i;
            cachedThreadPool.execute(() -> {
                System.out.println(Thread.currentThread().getName() + ", currentIndex is : " + currentIndex);
                countDownLatch.countDown();
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("全部線程執行完畢");
    }

上面的Demo中創建了一個無邊界限制的線程池,同時使用了一個多線程輔助類CountDownLatch,關於該類的使用,後面會有介紹。

執行結果:

public static void main(String[] args) {
        createCachedThreadPool();
    }
pool-1-thread-1, currentIndex is : 0
pool-1-thread-5, currentIndex is : 4
pool-1-thread-4, currentIndex is : 3
pool-1-thread-3, currentIndex is : 2
pool-1-thread-2, currentIndex is : 1
pool-1-thread-9, currentIndex is : 8
pool-1-thread-8, currentIndex is : 7
pool-1-thread-7, currentIndex is : 6
pool-1-thread-6, currentIndex is : 5
pool-1-thread-5, currentIndex is : 9
全部線程執行完畢

newFixedThreadPool

創建一個可重用固定線程數的線程池,以共享的無界隊列方式來運行這些線程。在任意點,在大多數 nThreads 線程會處於處理任務的活動狀態。如果在所有線程處於活動狀態時提交附加任務,則在有可用線程之前,附加任務將在隊列中等待。

/**
     * 創建固定大小的線程池
     */
    public static void createFixedThreadPool() {
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
        final CountDownLatch countDownLatch = new CountDownLatch(5);
        for (int i = 0; i < 5; i++) {
            final int currentIndex = i;
            fixedThreadPool.execute(() -> {
                System.out.println(Thread.currentThread().getName() + ", currentIndex is : " + currentIndex);
                countDownLatch.countDown();
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("全部線程執行完畢");
    }
public static void main(String[] args) {
        createFixedThreadPool();
    }
pool-1-thread-4, currentIndex is : 3
pool-1-thread-5, currentIndex is : 4
pool-1-thread-2, currentIndex is : 1
pool-1-thread-1, currentIndex is : 0
pool-1-thread-3, currentIndex is : 2
全部線程執行完畢

newScheduledThreadPool

創建一個線程池,它可安排在給定延遲後運行命令或者定期地執行。

    /**
     * 創建給定延遲後運行命令或者定期地執行的線程池
     */
    public static void createScheduledThreadPool() {
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
        final CountDownLatch countDownLatch = new CountDownLatch(5);
        for (int i = 0; i < 5; i++) {
            final int currentIndex = i;
            //定時執行一次的任務,延遲1s後執行
            scheduledThreadPool.schedule(() -> {
                System.out.println(Thread.currentThread().getName() + ", currentIndex is : " + currentIndex);
                countDownLatch.countDown();
            }, 1, TimeUnit.SECONDS);
            //週期性地執行任務,延遲2s後,每3s一次地週期性執行任務
            scheduledThreadPool.scheduleAtFixedRate(() -> System.out.println(Thread.currentThread().getName() + "every 3s"), 2, 3, TimeUnit.SECONDS);
        }
    }

這裏創建了一個調度的線程池,執行兩個任務,第一個任務延遲1秒後執行,第二個任務爲週期性任務,延遲2秒後,每三秒執行一次


執行結果:

public static void main(String[] args) {
        createScheduledThreadPool();
    }
pool-1-thread-1, currentIndex is : 0
pool-1-thread-2, currentIndex is : 1
pool-1-thread-3, currentIndex is : 2
pool-1-thread-2, currentIndex is : 3
pool-1-thread-4, currentIndex is : 4
pool-1-thread-5every 3s
pool-1-thread-2every 3s
pool-1-thread-3every 3s
pool-1-thread-1every 3s
pool-1-thread-5every 3s
pool-1-thread-2every 3s
pool-1-thread-4every 3s
pool-1-thread-4every 3s
pool-1-thread-3every 3s
pool-1-thread-2every 3s

可以看到,第一個任務執行完畢後,開始執行定時調度型任務

該線程池提供了多個方法:

  • 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()方法的週期時間間隔是以上一個任務開始執行到下一個任務開始執行的間隔,也就是這一些任務系列的觸發時間都是可預知的。

newSingleThreadExecutor

創建一個單線程的線程池,以無界隊列方式來運行該線程。當多個任務提交到單線程線程池中,線程池將逐個去進行執行,未執行的任務將放入無界隊列進行等待。

/**
     * 創建單線程的線程池
     */
    public static void createSingleThreadPool() {
        ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
        singleThreadPool.execute(()-> System.out.println(Thread.currentThread().getName()));
    }

執行結果:

public static void main(String[] args) {
        createSingleThreadPool();
    }
pool-1-thread-1
  •  

四種線程池對比

 

線程池方法 初始化線程池數 最大線程池數 線程池中線程存活時間 時間單位 工作隊列
newCachedThreadPool 0 Integer.MAX_VALUE 60 SynchronousQueue
newFixedThreadPool 入參指定大小 入參指定大小 0 毫秒 LinkedBlockingQueue
newScheduledThreadPool 入參指定大小 Integer.MAX_VALUE 0 微秒 DelayedWorkQueue
newSingleThreadExecutor 1 1 0 毫秒 LinkedBlockingQueue

ThreadPoolExecutor創建線程池

Executors類提供4個靜態工廠方法:newCachedThreadPool()、newFixedThreadPool(int)、newSingleThreadExecutor和newScheduledThreadPool(int)。這些方法最終都是通過ThreadPoolExecutor類來完成的,當有一些場景需要更細粒度的控制的線程池,可以使用ThreadPoolExecutor方法創建線程池。

/**
     * 使用ThreadPoolExecutor創建線程池
     */
    public void createThreadPoolExecutor() {

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 10L, TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(1000),
                new ThreadPoolExecutor.AbortPolicy());

        final CountDownLatch countDownLatch = new CountDownLatch(8);
        for (int i = 0; i < 8; i++) {
            final int currentIndex = i;
            System.out.println("提交第" + i + "個線程");
            threadPoolExecutor.execute(() -> {
                System.out.println(Thread.currentThread().getName() + ", currentIndex is : " + currentIndex);
                countDownLatch.countDown();
            });
        }
        System.out.println("全部提交完畢");
        try {
            System.out.println("準備等待線程池任務執行完畢");
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("全部線程執行完畢");
    }

執行結果:

提交第0個線程
提交第1個線程
提交第2個線程
提交第3個線程
提交第4個線程
pool-1-thread-2, currentIndex is : 1
提交第5個線程
pool-1-thread-4, currentIndex is : 3
pool-1-thread-3, currentIndex is : 2
pool-1-thread-1, currentIndex is : 0
pool-1-thread-3, currentIndex is : 5
pool-1-thread-5, currentIndex is : 4
提交第6個線程
提交第7個線程
pool-1-thread-2, currentIndex is : 6
pool-1-thread-2, currentIndex is : 7
全部提交完畢
準備等待線程池任務執行完畢
全部線程執行完畢

接下來看一下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 - 池中所保存的線程數,包括空閒線程,必須大於或等於0。
  • maximumPoolSize - 池中允許的最大線程數,必須大於或等於corePoolSize。
  • keepAliveTime - 線程存活時間,當線程數大於核心時,此爲終止前多餘的空閒線程等待新任務的最長時間。
  • unit - keepAliveTime 參數的時間單位,必須大於或等於0。
  • workQueue - 工作隊列,執行前用於保持任務的隊列。此隊列僅保持由 execute 方法提交的 Runnable 任務。
  • threadFactory - 執行程序創建新線程時使用的工廠,默認爲DefaultThreadFactory類。
  • handler - 拒絕策略,由於超出線程範圍和隊列容量而使執行被阻塞時所使用的處理程序,默認策略爲ThreadPoolExecutor.AbortPolicy。

各個參數詳細解釋:

  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(任務隊列):用於傳輸和保存等待執行任務的阻塞隊列。可以使用此隊列與線程池進行交互:
    • 如果運行的線程數少於 corePoolSize,則 Executor 始終首選添加新的線程,而不進行排隊。
    • 如果運行的線程數等於或多於 corePoolSize,則 Executor 始終首選將請求加入隊列,而不添加新的線程。
    • 如果無法將請求加入隊列,則創建新的線程,除非創建此線程超出maximumPoolSize,在這種情況下,任務將被拒絕。
  6. threadFactory(線程工廠):用於創建新線程。由同一個threadFactory創建的線程,屬於同一個ThreadGroup,創建的線程優先級都爲Thread.NORM_PRIORITY,以及是非守護進程狀態。threadFactory創建的線程也是採用new Thread()方式,threadFactory創建的線程名都具有統一的風格:pool-m-thread-n(m爲線程池的編號,n爲線程池內的線程編號);
  7. handler(線程飽和策略):當線程池和隊列都滿了,則表明該線程池已達飽和狀態。
    • ThreadPoolExecutor.AbortPolicy:處理程序遭到拒絕,則直接拋出運行時異常RejectedExecutionException。(默認策略)
    • ThreadPoolExecutor.CallerRunsPolicy:調用者所在線程來運行該任務,此策略提供簡單的反饋控制機制,能夠減緩新任務的提交速度。
    • ThreadPoolExecutor.DiscardPolicy:無法執行的任務將被刪除。
    • ThreadPoolExecutor.DiscardOldestPolicy:如果執行程序尚未關閉,則位於工作隊列頭部的任務將被刪除,然後重新嘗試執行任務(如果再次失敗,則重複此過程)。

排隊策略:

  1. 直接提交。工作隊列的默認選項是SynchronousQueue,它將任務直接提交給線程而不保持它們。在此,如果不存在可用於立即運行任務的線程,則試圖把任務加入隊列將失敗,因此會構造一個新的線程。此策略可以避免在處理可能具有內部依賴性的請求集時出現鎖。直接提交通常要求無界 maximumPoolSizes以避免拒絕新提交的任務。當命令以超過隊列所能處理的平均數連續到達時,此策略允許無界線程具有增長的可能性。
  2. 無界隊列。使用無界隊列(例如,不具有預定義容量的 LinkedBlockingQueue)將導致在所有 corePoolSize 線程都忙時新任務在隊列中等待。這樣,創建的線程就不會超過 corePoolSize。(因此,maximumPoolSize 的值也就無效了。)當每個任務完全獨立於其他任務,即任務執行互不影響時,適合於使用無界隊列;例如,在 Web 頁服務器中。這種排隊可用於處理瞬態突發請求,當命令以超過隊列所能處理的平均數連續到達時,此策略允許無界線程具有增長的可能性。
  3. 有界隊列。當使用有限的 maximumPoolSizes 時,有界隊列(如 ArrayBlockingQueue)有助於防止資源耗盡,但是可能較難調整和控制。隊列大小和最大池大小可能需要相互折衷:使用大型隊列和小型池可以最大限度地降低 CPU 使用率、操作系統資源和上下文切換開銷,但是可能導致人工降低吞吐量。如果任務頻繁阻塞(例如,如果它們是 I/O 邊界),則系統可能爲超過您許可的更多線程安排時間。使用小型隊列通常要求較大的池大小,CPU 使用率較高,但是可能遇到不可接受的調度開銷,這樣也會降低吞吐量。

工作隊列對比

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:按過去執行已提交任務的順序發起一個有序的關閉,但是不接受新任務。如果已經關閉,則調用沒有其他作用。
  • shutdownNow:嘗試停止所有的活動執行任務、暫停等待任務的處理,並返回等待執行的任務列表。在從此方法返回的任務隊列中排空(移除)這些任務。

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


線程池大小設置

如何合理地估算線程池大小,這個問題是比較複雜的,比較粗糙的估算方式:

  • 如果是CPU密集型應用,則線程池大小設置爲N+1
  • 如果是IO密集型應用,則線程池大小設置爲2N+1

但是根據我在實際應用場景的經驗,這種估算有時並不準確,這裏不展開討論線程池大小的設置,可以看一下這一篇文章的分析:如何合理地估算線程池大小

線程池的狀態監控

利用線程池提供的參數進行監控,參數如下:

  • getTaskCount:返回曾計劃執行的近似任務總數。因爲在計算期間任務和線程的狀態可能動態改變,所以返回值只是一個近似值。
  • getCompletedTaskCount:返回已完成執行的近似任務總數。因爲在計算期間任務和線程的狀態可能動態改變,所以返回值只是一個近似值,但是該值在整個連續調用過程中不會減少。
  • getLargestPoolSize:線程池曾經創建過的最大線程數量,通過這個數據可以知道線程池是否滿過。如等於線程池的最大大小,則表示線程池曾經滿了。
  • getPoolSize:線程池的線程數量。
  • getActiveCount:返回主動執行任務的近似線程數。

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

使用ThreadPoolExecutor直接創建線程池時,可以使用第三方的ThreadFactory,或者自己實現ThreadFactory接口,拓展更多的屬性,例如設置線程名稱、執行開始時間、優先級等等。

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