【多線程】說說線程池

前言

線程池內部是多個線程的集合,在創建初期,線程池會創建出多個空閒的線程,當有一個任務需要執行時,線程池會選擇出一個線程去執行它,執行結束後,該線程不會被銷燬,而是可以繼續複用。

使用線程池可以大大減少線程頻繁創建與銷燬的開銷,降低了系統資源的消耗。當任務來臨時,直接複用之前的線程,而不是先創建,提高了系統的響應速度。此外,線程池可以控制最大的併發數,避免資源的過度消耗。


簡單實例

先給出一個線程池的簡單例子:

package com.xue.testThreadPool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 4; i++) {
            int finalI = i;
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "正在執行任務" + finalI);
                }
            });
        }
        threadPool.shutdown();
    }
}

輸出如下:

可見,2個線程總共執行了4個任務,線程得到了複用。


線程池的核心參數

這些核心參數位於ThreadPoolExecutor的構造方法中:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
  • corePoolSize               核心線程數,或者說常駐線程數,線程池中最少線程數
  • maximumPoolSize      最大線程數
  • keepAliveTime             空閒線程的存活時間,線程池中當前線程數大於corePoolSize時,那些空閒時間達到keepAliveTime的空閒線程,它們將會被銷燬掉
  • TimeUnit                       keepAliveTime的時間單位
  • workQueue                   任務隊列,存放未被執行的任務
  • threadFactory               創建線程的工廠
  • handler                          拒絕策略,當前線程數≥最大線程數且任務隊列滿的時候,對後續任務的拒絕方式

線程池的種類

不同的線程池有不同的適用場景,本質上都是在Executors類中實例化一個ThreadPoolExecutor對象,只是傳入的參數不一樣罷了。

線程池的種類有以下幾種:

newFixedThreadPool

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

創建一個固定大小的線程池,即核心線程數等於最大線程數,每個線程的存活時間和線程池的壽命一致,線程池滿負荷運作時,多餘的任務會加入到無界的阻塞隊列中,newFixedThreadPool可以很好的控制線程的併發量。

newCachedThreadPool

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

創建一個可以無限擴大的線程池,當任務來臨時,有空閒線程就去執行,否則立即創建一個線程。當線程的空閒時間超過1分鐘時,銷燬該線程。適用於執行任務較少且需要快速執行的場景,即短期異步任務。

newSingleThreadExecutor

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

創建一個大小爲1的線程池,用於順序執行任務。

newScheduledThreadPool

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

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

創建一個初始大小爲corePoolSize的線程池,線程池的存活時間沒有限制,newScheduledThreadPool中的schedule方法用於延時執行任務,scheduleAtFixedRate用於週期性地執行任務。


 線程池執行任務的流程

  • 當線程池中線程數小於corePoolSize時,新提交任務將創建一個新線程執行任務,即使此時線程池中存在空閒線程。

  • 當線程池中線程數達到corePoolSize時,新提交任務將被放入workQueue中,等待線程池中任務調度執行 。

  • 當workQueue已滿,且maximumPoolSize > corePoolSize時,新提交任務會創建新線程執行任務。

  • 當workQueue已滿,且提交任務數超過maximumPoolSize,任務由RejectedExecutionHandler處理。

  • 當線程池中線程數超過corePoolSize,且超過這部分的空閒時間達到keepAliveTime時,回收這些線程。

  • 當設置allowCoreThreadTimeOut(true)時,線程池中corePoolSize範圍內的線程空閒時間達到keepAliveTime也將回收。

使用更加直觀的流程圖來描述:

注:此章節參考通俗易懂,各常用線程池執行的-流程圖


工作隊列

工作隊列用來存儲提交的任務,工作隊列一般使用的都是阻塞隊列。阻塞隊列可以保證任務隊列中沒有任務時阻塞獲取任務的線程,使得線程進入wait狀態,釋放cpu資源。當隊列中有任務時才喚醒對應線程從隊列中取出消息進行執行。

阻塞隊列一般由以下幾種:

LinkedBlockingQueue  

由單鏈表實現的無界阻塞隊列,遵循FIFO。注意這裏的無界是因爲其記錄隊列大小的數據類型是int,那麼隊列長度的最大值就是恐怖的Integer.MAX_VALUE,這個值已經很大了,因此可以將之稱爲無界隊列。不過該隊列也提供了有參構造函數,可以手動指定其隊列大小,否則使用默認的int最大值。

LinkedBlockingQueue只能從head取元素,從tail添加元素。添加元素和獲取元素都有獨立的鎖,也就是說它是讀寫分離的,讀寫操作可以並行執行。LinkedBlockingQueue採用可重入鎖(ReentrantLock)來保證在併發情況下的線程安全。

當線程數目達到corePoolSize時,後續的任務會直接加入到LinkedBlockingQueue中,在不指定其隊列大小的情況下,該隊列永遠也不會滿,可能內存滿了,隊列都不會滿,此時maximumPoolSize和拒絕策略將不會有任何意義

ArrayBlockingQueue

由數組實現的有界阻塞隊列,同樣遵循FIFO,必須制定隊列大小。使用全局獨佔鎖的方式,使得在同一時間只有一個線程能執行入隊或出隊操作,相比於LinkedBlockingQueue,ArrayBlockingQueue鎖的力度很大。

SynchronousQueue

是一個沒有容量的隊列,當然也可以稱爲單元素隊列。會將任務直接傳遞給消費者,添加任務時,必須等待前一個被添加的任務被消費掉,即take動作等待put動作,put動作等待take動作,put與take是循環往復的

如果線程拒絕執行該隊列中的任務,或者說沒有線程來執行。那麼舊任務無法被執行,新任務也無法被添加,線程池將陷入一種尷尬的境地。因此,該隊列一般需要maximumPoolSize爲Integer.MAX_VALUE,有一個任務到來,就立馬新起一個線程執行,newCachedThreadPool就是使用的這種組合。

關於這些阻塞隊列的源碼解析,可能需要另開篇幅。


線程工廠

先看一下,ThreadPoolExecutor構造方法中默認使用的線程工廠

    static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }

        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

defaultThreadFactory對於線程的命名方式爲“pool-”+pool的自增序號+"-thread-"+線程的自增序號,這也印證了在簡單實例的章節中,輸出Thread.getCurrentThread.getName()是“pool-1-thread-1”的樣式

默認線程工廠給線程的取名沒有太多的意義,在實際開發中,我們一般會給線程取個比較有識別度的名稱,方便出現問題時的排查。


拒絕策略

如果當工作隊列已滿,且線程數目達到maximumPoolSize後,依然有任務到來,那麼此時線程池就會採取拒絕策略。

ThreadPoolExecutor中提供了4種拒絕策略。

AbortPolicy

     private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();   

     public static class AbortPolicy implements RejectedExecutionHandler {
 
            public AbortPolicy() { }

            public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
            }
    }

這是線程池的默認拒絕策略,直接會丟棄任務並拋出RejectedExecutionException異常。

DiscardPolicy

    public static class DiscardPolicy implements RejectedExecutionHandler {

        public DiscardPolicy() { }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }
    }

丟棄後續提交的任務,但不拋出異常。建議在一些無關緊要的場景中使用此拒絕策略,否則無法及時發現系統的異常狀態。

DiscardOldestPolicy

    public static class DiscardOldestPolicy implements RejectedExecutionHandler {

        public DiscardOldestPolicy() { }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }
    }

從源碼中可以看到,此拒絕策略會丟棄隊列頭部的任務,然後將後續提交的任務加入隊列中。

CallerRunsPolicy

    public static class CallerRunsPolicy implements RejectedExecutionHandler {

        public CallerRunsPolicy() { }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }
    }

由調用線程執行該任務,即提交任務的線程,一般是主線程。


如何配置最大線程數

CPU密集型任務

CPU密集指的是需要進行大量的運算,一般沒有什麼阻塞。

儘量使用較小的線程池,大小一般爲CPU核心數+1。因爲CPU密集型任務使得CPU使用率很高,若開過多的線程數,會造成CPU過度切換。

IO密集型任務

IO密集指的是需要進行大量的IO,阻塞十分嚴重,可以掛起被阻塞的線程,開啓新的線程幹別的事情。

可以使用稍大的線程池,大小一般爲CPU核心數*2。IO密集型任務CPU使用率並不高,因此可以讓CPU在等待IO的時候有其他線程去處理別的任務,充分利用CPU時間。

當然,依據IO密集的程度,可以在兩倍的基礎上進行相應的擴大與縮小。


總結

這篇文章粗淺地說明了線程池的種類、執行流程、工作隊列與拒絕策略等,但缺少對線程池源碼的分析,這個會另開篇幅進行說明。

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