執行緒與執行緒池的那些事之執行緒池篇(萬字長文)

第十六封發表於2021-06-21

本文關鍵字:

執行緒執行緒池單執行緒多執行緒執行緒池的好處執行緒回收建立方式核心引數底層機制拒絕策略,引數設定,動態監控執行緒隔離

執行緒和執行緒池相關的知識,是Java學習或者面試中一定會遇到的知識點,本篇我們會從執行緒和程式,並行與併發,單執行緒和多執行緒等,一直講解到執行緒池,執行緒池的好處,建立方式,重要的核心引數,幾個重要的方法,底層實現,拒絕策略,引數設定,動態調整,執行緒隔離等等。主要的大綱如下:

執行緒池的好處

執行緒池,使用了池化思想來管理執行緒,池化技術就是為了最大化效益,最小化使用者風險,將資源統一放在一起管理的思想。這種思想在很多地方都有使用到,不僅僅是計算機,比如金融,企業管理,裝置管理等。

為什麼要執行緒池?如果在併發的場景,編碼人員根據需求來建立執行緒池,可能會有以下的問題:

  • 我們很難確定系統有多少執行緒在執行,如果使用就建立,不使用就銷燬,那麼建立和銷燬執行緒的消耗也是比較大的
  • 假設來了很多請求,可能是爬蟲,瘋狂建立執行緒,可能把系統資源耗盡。

實現執行緒池有什麼好處呢?

  • 降低資源消耗:池化技術可以重複利用已經建立的執行緒,降低執行緒建立和銷燬的損耗。
  • 提高響應速度:利用已經存在的執行緒進行處理,少去了建立執行緒的時間
  • 管理執行緒可控:執行緒是稀缺資源,不能無限建立,執行緒池可以做到統一分配和監控
  • 擴充其他功能:比如定時執行緒池,可以定時執行任務

其實池化技術,用在比較多地方,比如:

  • 資料庫連線池:資料庫連線是稀缺資源,先建立好,提高響應速度,重複利用已有的連線
  • 例項池:先建立好物件放到池子裡面,迴圈利用,減少來回建立和銷燬的消耗

執行緒池相關的類

下面是與執行緒池相關的類的繼承關係:

Executor

Executor 是頂級介面,裡面只有一個方法execute(Runnable command),定義的是排程執行緒池來執行任務,它定義了執行緒池的基本規範,執行任務是它的天職。

ExecutorService

ExecutorService 繼承了Executor,但是它仍然是一個介面,它多了一些方法:

  • void shutdown():關閉執行緒池,會等待任務執行完。
  • List<Runnable> shutdownNow():立刻關閉執行緒池,嘗試停止所有正在積極執行的任務,停止等待任務的處理,並返回一個正在等待執行的任務列表(還沒有執行的)
  • boolean isShutdown():判斷執行緒池是不是已經關閉,但是可能執行緒還在執行。
  • boolean isTerminated():在執行shutdown/shutdownNow之後,所有的任務已經完成,這個狀態就是true。
  • boolean awaitTermination(long timeout, TimeUnit unit):執行shutdown之後,阻塞等到terminated狀態,除非超時或者被打斷。
  • <T> Future<T> submit(Callable<T> task): 提交一個有返回值的任務,並且返回該任務尚未有結果的Future,呼叫future.get()方法,可以返回任務完成的時候的結果。
  • <T> Future<T> submit(Runnable task, T result):提交一個任務,傳入返回結果,這個result沒有什麼作用,只是指定型別和一個返回的結果。
  • Future<?> submit(Runnable task): 提交任務,返回Future
  • <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks):批量執行tasks,獲取Future的list,可以批量提交任務。
  • <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit):批量提交任務,並指定超時時間
  • <T> T invokeAny(Collection<? extends Callable<T>> tasks): 阻塞,獲取第一個完成任務的結果值,
  • <T> T invokeAny(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit):阻塞,獲取第一個完成結果的值,指定超時時間

可能有同學對前面的<T> Future<T> submit(Runnable task, T result)有疑問,這個reuslt有什麼作用?

其實它沒有什麼作用,只是持有它,任務完成後,還是呼叫 future.get()返回這個結果,用result new 了一個 ftask,其內部其實是使用了Runnable的包裝類 RunnableAdapter,沒有對result做特殊的處理,呼叫 call() 方法的時候,直接返回這個結果。(Executors 中具體的實現)

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

    static final class RunnableAdapter<T> implements Callable<T> {
        final Runnable task;
        final T result;
        RunnableAdapter(Runnable task, T result) {
            this.task = task;
            this.result = result;
        }
        public T call() {
            task.run();
            // 返回傳入的結果
            return result;
        }
    }

還有一個方法值得一提:invokeAny(): 在 ThreadPoolExecutor中使用ExecutorService 中的方法 invokeAny() 取得第一個完成的任務的結果,當第一個任務執行完成後,會呼叫 interrupt() 方法將其他任務中斷。

注意,ExecutorService是介面,裡面都是定義,並沒有涉及實現,而前面的講解都是基於它的名字(規定的規範)以及它的普遍實現來說的。

可以看到 ExecutorService 定義的是執行緒池的一些操作,包括關閉,判斷是否關閉,是否停止,提交任務,批量提交任務等等。

AbstractExecutorService

AbstractExecutorService 是一個抽象類,實現了 ExecutorService介面,這是大部分執行緒池的基本實現,定時的執行緒池先不關注,主要的方法如下:

不僅實現了submitinvokeAllinvokeAny 等方法,而且提供了一個 newTaskFor 方法用於構建 RunnableFuture 物件,那些能夠獲取到任務返回結果的物件都是通過 newTaskFor 來獲取的。不展開裡面所有的原始碼的介紹,僅以submit()方法為例:

    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        // 封裝任務
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        // 執行任務
        execute(ftask);
        // 返回 RunnableFuture 物件
        return ftask;
    }

但是在 AbstractExecutorService 是沒有對最最重要的方法進行實現的,也就是 execute() 方法。執行緒池具體是怎麼執行的,這個不同的執行緒池可以有不同的實現,一般都是繼承 AbstractExecutorService (定時任務有其他的介面),我們最最常用的就是ThreadPoolExecutor

ThreadPoolExecutor

重點來了!!! ThreadPoolExecutor 一般就是我們平時常用到的執行緒池類,所謂建立執行緒池,如果不是定時執行緒池,就是使用它。

先看ThreadPoolExecutor的內部結構(屬性):

public class ThreadPoolExecutor extends AbstractExecutorService {
    // 狀態控制,主要用來控制執行緒池的狀態,是核心的遍歷,使用的是原子類
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
  	// 用來表示執行緒數量的位數(使用的是位運算,一部分表示執行緒的數量,一部分表示執行緒池的狀態)
    // SIZE = 32 表示32位,那麼COUNT_BITS就是29位
    private static final int COUNT_BITS = Integer.SIZE - 3;
  	// 執行緒池的容量,也就是27位表示的最大值
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

    // 狀態量,儲存在高位,32位中的前3位
  	// 111(第一位是符號位,1表示負數),執行緒池執行中
    private static final int RUNNING    = -1 << COUNT_BITS; 
  	// 000
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
  	// 001
    private static final int STOP       =  1 << COUNT_BITS;
  	// 010
    private static final int TIDYING    =  2 << COUNT_BITS;
  	// 011
    private static final int TERMINATED =  3 << COUNT_BITS;

    // 取出執行狀態
    private static int runStateOf(int c)     { return c & ~CAPACITY; }
  	// 取出執行緒數量
    private static int workerCountOf(int c)  { return c & CAPACITY; }
  	// 用執行狀態和執行緒數獲取ctl
    private static int ctlOf(int rs, int wc) { return rs | wc; }
  	
  	// 任務等待佇列
    private final BlockingQueue<Runnable> workQueue;
  	// 可重入主鎖(保證一些操作的執行緒安全)
    private final ReentrantLock mainLock = new ReentrantLock();
  	// 執行緒的集合
    private final HashSet<Worker> workers = new HashSet<Worker>();
  
  	// 在Condition中,用await()替換wait(),用signal()替換notify(),用signalAll()替換notifyAll(),
    // 傳統執行緒的通訊方式,Condition都可以實現,Condition和傳統的執行緒通訊沒什麼區別,Condition的強大之處在於它可以為多個執行緒間建立不同的Condition
    private final Condition termination = mainLock.newCondition();
  
  	// 最大執行緒池大小
    private int largestPoolSize;
  	// 完成的任務數量
    private long completedTaskCount;
  	// 執行緒工廠
    private volatile ThreadFactory threadFactory;
  	// 任務拒絕處理器
    private volatile RejectedExecutionHandler handler;
 		// 非核心執行緒的存活時間
    private volatile long keepAliveTime;
  	// 允許核心執行緒的超時時間
    private volatile boolean allowCoreThreadTimeOut;
 		// 核心執行緒數
    private volatile int corePoolSize;
		// 工作執行緒最大容量
    private volatile int maximumPoolSize;
 		// 預設的拒絕處理器(丟棄任務)
  	private static final RejectedExecutionHandler defaultHandler =
        new AbortPolicy();
  	// 執行時關閉許可
    private static final RuntimePermission shutdownPerm =
        new RuntimePermission("modifyThread");
  	// 上下文
    private final AccessControlContext acc;
  	// 只有一個執行緒
    private static final boolean ONLY_ONE = true;
}

執行緒池狀態

從上面的程式碼可以看出,用一個32位的物件儲存執行緒池的狀態以及執行緒池的容量,高3位是執行緒池的狀態,而剩下的29位,則是儲存執行緒的數量:

    // 狀態量,儲存在高位,32位中的前3位
  	// 111(第一位是符號位,1表示負數),執行緒池執行中
    private static final int RUNNING    = -1 << COUNT_BITS; 
  	// 000
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
  	// 001
    private static final int STOP       =  1 << COUNT_BITS;
  	// 010
    private static final int TIDYING    =  2 << COUNT_BITS;
  	// 011
    private static final int TERMINATED =  3 << COUNT_BITS;

各種狀態之間是不一樣的,他們的狀態之間變化如下:

  • RUNNING:執行狀態,可以接受任務,也可以處理任務
  • SHUTDOWN:不可以接受任務,但是可以處理任務
  • STOP:不可以接受任務,也不可以處理任務,中斷當前任務
  • TIDYING:所有執行緒停止
  • TERMINATED:執行緒池的最後狀態

Worker 實現

執行緒池,肯定得有池子,並且是放執行緒的地方,在 ThreadPoolExecutor 中表現為 Worker,這是內部類:

執行緒池其實就是 Worker (打工人,不斷的領取任務,完成任務)的集合,這裡使用的是 HashSet:

private final HashSet<Worker> workers = new HashSet<Worker>();

Worker 怎麼實現的呢?

Worker 除了繼承了 AbstractQueuedSynchronizer,也就是 AQSAQS 本質上就是個佇列鎖,一個簡單的互斥鎖,一般是在中斷或者修改 worker 狀態的時候使用。

內部引入AQS,是為了執行緒安全,執行緒執行任務的時候,呼叫的是runWorker(Worker w),這個方法不是worker的方法,而是 ThreadPoolExecutor的方法。從下面的程式碼可以看出,每次修改Worker的狀態的時候,都是執行緒安全的。Worker裡面,持有了一個執行緒Thread,可以理解為是對執行緒的封裝。

至於runWorker(Worker w)是怎麼執行的?先保持這個疑問,後面詳細講解。

    // 實現 Runnable,封裝了執行緒
    private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
        // 序列化id
        private static final long serialVersionUID = 6138294804551838833L;

        // worker執行的執行緒
        final Thread thread;
        
        // 初始化任務,有可能是空的,如果任務不為空的時候,其他進來的任務,可以直接執行,不在新增到任務佇列
        Runnable firstTask;
        // 執行緒任務計數器
        volatile long completedTasks;

        // 指定一個任務讓工人忙碌起來,這個任務可能是空的
        Worker(Runnable firstTask) {
          	// 初始化AQS佇列鎖的狀態
            setState(-1); // 禁止中斷直到 runWorker
            this.firstTask = firstTask;
            // 從執行緒工廠,取出一個執行緒初始化
            this.thread = getThreadFactory().newThread(this);
        }

        // 實際上執行呼叫的是runWorker
        public void run() {
          	// 不斷迴圈獲取任務進行執行
            runWorker(this);
        }

        // 0表示沒有被鎖
        // 1表示被鎖的狀態
        protected boolean isHeldExclusively() {
            return getState() != 0;
        }
        // 獨佔,嘗試獲取鎖,如果成功返回true,失敗返回false
        protected boolean tryAcquire(int unused) {
            // CAS 樂觀鎖
            if (compareAndSetState(0, 1)) {
                // 成功,當前執行緒獨佔鎖
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
        // 獨佔方式,嘗試釋放鎖
        protected boolean tryRelease(int unused) {
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
        // 上鎖,呼叫的是AQS的方法
        public void lock()        { acquire(1); }
        // 嘗試上鎖
        public boolean tryLock()  { return tryAcquire(1); }
        // 解鎖
        public void unlock()      { release(1); }
        // 是否鎖住
        public boolean isLocked() { return isHeldExclusively(); }

        // 如果開始可就中斷
        void interruptIfStarted() {
            Thread t;
            if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                }
            }
        }
    }

任務佇列

除了放執行緒池的地方,要是任務很多,沒有那麼多執行緒,肯定需要一個地方放任務,充當緩衝作用,也就是任務佇列,在程式碼中表現為:

private final BlockingQueue<Runnable> workQueue;

拒絕策略和處理器

計算機的記憶體總是有限的,我們不可能一直往佇列裡面增加內容,所以執行緒池為我們提供了選擇,可以選擇多種佇列。同時當任務實在太多,佔滿了執行緒,並且把任務佇列也佔滿的時候,我們需要做出一定的反應,那就是拒絕還是丟擲錯誤,丟掉任務?丟掉哪些任務,這些都是可能需要定製的內容。

如何建立執行緒池

關於如何建立執行緒池,其實 ThreadPoolExecutor提供了構造方法,主要引數如下,不傳的話會使用預設的:

  • 核心執行緒數:核心執行緒數,一般是指常駐的執行緒,沒有任務的時候通常也不會銷燬
  • 最大執行緒數:執行緒池允許建立的最大的執行緒數量
  • 非核心執行緒的存活時間:指的是沒有任務的時候,非核心執行緒能夠存活多久
  • 時間的單位:存活時間的單位
  • 存放任務的佇列:用來存放任務
  • 執行緒工廠
  • 拒絕處理器:如果新增任務失敗,將由該處理器處理
	// 指定核心執行緒數,最大執行緒數,非核心執行緒沒有任務的存活時間,時間單位,任務佇列    
	public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
	  // 指定核心執行緒數,最大執行緒數,非核心執行緒沒有任務的存活時間,時間單位,任務佇列,執行緒池工廠    
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             threadFactory, defaultHandler);
    }
	  // 指定核心執行緒數,最大執行緒數,非核心執行緒沒有任務的存活時間,時間單位,任務佇列,拒絕任務處理器
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), handler);
    }
		// 最後其實都是呼叫了這個方法
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
      ...
    }

其實,除了顯示的指定上面的引數之外,JDK也封裝了一些直接建立執行緒池的方法給我們,那就是Executors:

		// 固定執行緒數量的執行緒池,無界的佇列
		public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      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));
    }
		// 動態調節,沒有核心執行緒,全部都是普通執行緒,每個執行緒存活60s,使用容量為1的阻塞佇列
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
	  // 定時任務執行緒池
    public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
        return new DelegatedScheduledExecutorService
            (new ScheduledThreadPoolExecutor(1));
    }

但是一般是不推薦使用上面別人封裝的執行緒池的哈!!!

執行緒池的底層引數以及核心方法

看完上面的建立引數大家可能會有點懵,但是沒關係,一一為大家道來:

可以看出,當有任務進來的時候,先判斷核心執行緒池是不是已經滿了,如果還沒有,將會繼續建立執行緒。注意,如果一個任務進來,建立執行緒執行,執行完成,執行緒空閒下來,這時候再來一個任務,是會繼續使用之前的執行緒,還是重新建立一個執行緒來執行呢?

答案是重新建立執行緒,這樣執行緒池可以快速達到核心執行緒數的規模大小,以便快速響應後面的任務。

如果執行緒數量已經到達核心執行緒數,來了任務,執行緒池的執行緒又都不是空閒狀態,那麼就會判斷佇列是不是滿的,倘若佇列還有空間,那麼就會把任務放進去佇列中,等待執行緒領取執行。

如果任務佇列已經滿了,放不下任務,那麼就會判斷執行緒數是不是已經到最大執行緒數了,要是還沒有到達,就會繼續建立執行緒並執行任務,這個時候建立的是非核心部分執行緒。

如果已經到達最大執行緒數,那麼就不能繼續建立執行緒了,只能執行拒絕策略,預設的拒絕策略是丟棄任務,我們可以自定義拒絕策略。

值得注意的是,倘若之前任務比較多,建立出了一些非核心執行緒,那麼任務少了之後,領取不到任務,過了一定時間,非核心執行緒就會銷燬,只剩下核心執行緒池的數量的執行緒。這個時間就是前面說的keepAliveTime

提交任務

提交任務,我們看execute(),會先獲取執行緒池的狀態和個數,要是執行緒個數還沒達到核心執行緒數,會直接新增執行緒,否則會放到任務佇列,如果任務佇列放不下,會繼續增加執行緒,但是不是增加核心執行緒。

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        // 獲取狀態和個數
        int c = ctl.get();
      	// 如果個數小於核心執行緒數
        if (workerCountOf(c) < corePoolSize) {
          	// 直接新增
            if (addWorker(command, true))
                return;
          	// 新增失敗則繼續獲取
            c = ctl.get();
        }
      	// 判斷執行緒池狀態是不是執行中,任務放到佇列中
        if (isRunning(c) && workQueue.offer(command)) {
          	// 再次檢查
            int recheck = ctl.get();
          	// 判斷執行緒池是不是還在執行
            if (! isRunning(recheck) && remove(command))
              	// 如果不是,那麼就拒絕並移除任務
                reject(command);
            else if (workerCountOf(recheck) == 0)
              	// 如果執行緒數為0,並且還在執行,那麼就直接新增
                addWorker(null, false);
        }else if (!addWorker(command, false))
          	// 新增任務佇列失敗,拒絕
            reject(command);
    }

上面的原始碼中,呼叫了一個重要的方法:addWorker(Runnable firstTask, boolean core),該方法主要是為了增加工作的執行緒,我們來看看它是如何執行的:

    private boolean addWorker(Runnable firstTask, boolean core) {
      	// 回到當前位置重試
        retry:
        for (;;) {
          	// 獲取狀態
            int c = ctl.get();
            int rs = runStateOf(c);

            // 大於SHUTDOWN說明執行緒池已經停止
          	// ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty()) 表示三個條件至少有一個不滿足
          	// 不等於SHUTDOWN說明是大於shutdown
          	// firstTask != null 任務不是空的
          	// workQueue.isEmpty() 佇列是空的
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                // 工作執行緒數
                int wc = workerCountOf(c);
              	// 是否符合容量
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
              	// 新增成功,跳出迴圈
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
              	// cas失敗,重新嘗試
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }

      	// 前面執行緒計數增加成功
        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
          	// 建立了一個worker,包裝了任務
            w = new Worker(firstTask);
            final Thread t = w.thread;
          	// 執行緒建立成功
            if (t != null) {
              	// 獲取鎖
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    // 再次確認狀態
                    int rs = runStateOf(ctl.get());
                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                      	// 如果執行緒已經啟動,失敗
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                      	// 新增執行緒到集合
                        workers.add(w);
                      	// 獲取大小
                        int s = workers.size();
                      	// 判斷最大執行緒池數量
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                      	// 已經新增工作執行緒
                        workerAdded = true;
                    }
                } finally {
                  	// 解鎖
                    mainLock.unlock();
                }
              	// 如果已經新增
                if (workerAdded) {
                  	// 啟動執行緒
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
          	// 如果沒有啟動
            if (! workerStarted)
              	// 失敗處理
                addWorkerFailed(w);
        }
        return workerStarted;
    }

處理任務

前面在介紹Worker這個類的時候,我們講解到其實它的run()方法呼叫的是外部的runWorker()方法,那麼我們來看看runWorkder()方法:

首先,它會直接處理自己的firstTask,這個任務並沒有在任務佇列裡面,而是它自己持有的:

final void runWorker(Worker w) {
  			// 當前執行緒
        Thread wt = Thread.currentThread();
  			// 第一個任務
        Runnable task = w.firstTask;
  			// 重置為null
        w.firstTask = null;
  			// 允許打斷
        w.unlock();
        boolean completedAbruptly = true;
        try {
           // 任務不為空,或者獲取的任務不為空
            while (task != null || (task = getTask()) != null) {
              	// 加鎖
                w.lock();
								//如果執行緒池停止,確保執行緒被中斷;
								//如果不是,確保執行緒沒有被中斷。這
								//在第二種情況下需要複查處理
								// shutdown - now競賽同時清除中斷
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                  	// 執行之前回撥方法(可以由我們自己實現)
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                      	// 執行任務
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                      	// 執行之後回撥方法
                        afterExecute(task, thrown);
                    }
                } finally {
                  	// 置為null
                    task = null;
                  	// 更新完成任務
                    w.completedTasks++;
                    w.unlock();
                }
            }
          	// 完成
            completedAbruptly = false;
        } finally {
          	// 處理執行緒退出相關工作
            processWorkerExit(w, completedAbruptly);
        }
    }

上面可以看到如果當前的任務是null,會去獲取一個task,我們看看getTask(),裡面涉及到了兩個引數,一個是是不是允許核心執行緒銷燬,另外一個是執行緒數是不是大於核心執行緒數,如果滿足條件,就從佇列中取出任務,如果超時取不到,那就返回空,表示沒有取到任務,沒有取到任務,就不會執行前面的迴圈,就會觸發執行緒銷燬processWorkerExit()等工作。

private Runnable getTask() {
  	// 是否超時
    boolean timedOut = false; // Did the last poll() time out?

    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // SHUTDOWN狀態繼續處理佇列中的任務,但是不接收新的任務
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }
      	// 執行緒數
        int wc = workerCountOf(c);

        // 是否允許核心執行緒超時或者執行緒數大於核心執行緒數
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
          	// 減少執行緒成功,就返回null,後面由processWorkerExit()處理
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
          	// 如果允許核心執行緒關閉,或者超過了核心執行緒,就可以在超時的時間內獲取任務,或者直接取出任務
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
          	// 如果能取到任務,那就肯定可以執行
            if (r != null)
                return r;
          	// 否則就獲取不到任務,超時了
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

銷燬執行緒

前面提到,如果執行緒當前任務為空,又允許核心執行緒銷燬,或者執行緒超過了核心執行緒數,等待了一定時間,超時了卻沒有從任務佇列獲取到任務的話,就會跳出迴圈執行到後面的執行緒銷燬(結束)程式。那銷燬執行緒的時候怎麼做呢?

    private void processWorkerExit(Worker w, boolean completedAbruptly) {
      	// 如果是突然結束的執行緒,那麼之前的執行緒數是沒有調整的,這裡需要調整
        if (completedAbruptly)
            decrementWorkerCount();
      	// 獲取鎖
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
      
        try {
          	// 完成的任務數
            completedTaskCount += w.completedTasks;
            // 移除執行緒
          	workers.remove(w);
        } finally {
          	// 解鎖
            mainLock.unlock();
        }
      	// 試圖停止
        tryTerminate();
      	// 獲取狀態
        int c = ctl.get();
      	// 比stop小,至少是shutdown
        if (runStateLessThan(c, STOP)) {
          	// 如果不是突然完成
            if (!completedAbruptly) {
              	// 最小值要麼是0,要麼是核心執行緒數,要是允許核心執行緒超時銷燬,那麼就是0
                int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
              	// 如果最小的是0或者佇列不是空的,那麼保留一個執行緒
                if (min == 0 && ! workQueue.isEmpty())
                    min = 1;
              	// 只要大於等於最小的執行緒數,就結束當前執行緒
                if (workerCountOf(c) >= min)
                    return; // replacement not needed
            }
          	// 否則的話,可能還需要新增工作執行緒
            addWorker(null, false);
        }
    }

如何停止執行緒池

停止執行緒池可以使用shutdown()或者shutdownNow()shutdown()可以繼續處理佇列中的任務,而shutdownNow()會立即清理任務,並返回未執行的任務。

    public void shutdown() {
        // 獲取鎖
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
          	// 檢查停止許可權
            checkShutdownAccess();
          	// 更新狀態
            advanceRunState(SHUTDOWN);
          	// 中斷所有執行緒
            interruptIdleWorkers();
          	// 回撥鉤子
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }
		// 立刻停止
   public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
     		// 獲取鎖
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
          	// 檢查停止許可權
            checkShutdownAccess();
          	// 更新狀態到stop
            advanceRunState(STOP);
          	// 中斷所有執行緒
            interruptWorkers();
            // 清理佇列
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
     		// 返回任務列表(未完成)
        return tasks;
    }

execute()和submit()方法

  • execute() 方法可以提交不需要返回值的任務,無法判斷任務是否被執行緒池執行是否成功
  • submit()方法用於提交需要返回值的任務。執行緒池會返回一個future型別的物件,通過這個物件,我們呼叫get()方法就可以阻塞,直到獲取到執行緒執行完成的結果,同時我們也可以使用有超時時間的等待方法get(long timeout,TimeUnit unit),這樣不管執行緒有沒有執行完成,如果到時間,也不會阻塞,直接返回null。返回的是RunnableFuture物件,繼承了Runnable, Future<V>兩個介面:
public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

執行緒池為什麼使用阻塞佇列?

阻塞佇列,首先是一個佇列,肯定具有先進先出的屬性。

而阻塞,則是這個模型的演化,一般佇列,可以用在生產消費者模型,也就是資料共享,有人往裡面放任務,有人不斷的往裡面取出任務,這是一個理想的狀態。

但是倘若不理想,產生任務和消費任務的速度不一樣,要是任務放在佇列裡面比較多,消費比較慢,還可以慢慢消費,或者生產者得暫停一下產生任務(阻塞生產者執行緒)。可以使用 offer(E o, long timeout, TimeUnit unit)設定等待的時間,如果在指定的時間內,還不能往佇列中加入BlockingQueue,則返回失敗,也可以使用put(Object),將物件放到阻塞佇列裡面,如果沒有空間,那麼這個方法會阻塞到有空間才會放進去。

如果消費速度快,生產者來不及生產,獲取任務的時候,可以使用poll(time),有資料則直接取出來,沒資料則可以等待time時間後,返回null。也可以使用take()取出第一個任務,沒有任務就會一直阻塞到佇列有任務為止。

上面說了阻塞佇列的屬性,那麼為啥要用呢?

  • 如果產生任務,來了就往佇列裡面放,資源很容易被耗盡。
  • 建立執行緒需要獲取鎖,這個一個執行緒池的全域性鎖,如果各個執行緒不斷的獲取鎖,解鎖,執行緒上下文切換之類的開銷也比較大,不如在佇列為空的時候,然一個執行緒阻塞等待。

常見的阻塞佇列

  • ArrayBlockingQueue:基於陣列實現,內部有一個定長的陣列,同時儲存著佇列頭和尾部的位置。
  • LinkedBlockingQueue:基於連結串列的阻塞對壘,生產者和消費者使用獨立的鎖,並行能力強,如果不指定容量,預設是無效容量,容易系統記憶體耗盡。
  • DelayQueue:延遲佇列,沒有大小限制,生產資料不會被阻塞,消費資料會,只有指定的延遲時間到了,才能從佇列中獲取到該元素。
  • PriorityBlockingQueue:基於優先順序的阻塞佇列,按照優先順序進行消費,內部控制同步的是公平鎖。
  • SynchronousQueue:沒有緩衝,生產者直接把任務交給消費者,少了中間的快取區。

執行緒池如何複用執行緒的?執行完成的執行緒怎麼處理

前面的原始碼分析,其實已經講解過這個問題了,執行緒池的執行緒呼叫的run()方法,其實呼叫的是runWorker(),裡面是死迴圈,除非獲取不到任務,如果沒有了任務firstTask並且從任務佇列中獲取不到任務,超時的時候,會再判斷是不是可以銷燬核心執行緒,或者超過了核心執行緒數,滿足條件的時候,才會讓當前的執行緒結束。

否則,一直都在一個迴圈中,不會結束。

我們知道start()方法只能呼叫一次,因此呼叫到run()方法的時候,呼叫外面的runWorker(),讓其在runWorker()的時候,不斷的迴圈,獲取任務。獲取到任務,呼叫任務的run()方法。

執行完成的執行緒會呼叫processWorkerExit(),前面有分析,裡面會獲取鎖,把執行緒數減少,從工作執行緒從集合中移除,移除掉之後,會判斷執行緒是不是太少了,如果是,會再加回來,個人以為是一種補救。

如何配置執行緒池引數?

一般而言,有個公式,如果是計算(CPU)密集型的任務,那麼核心執行緒數設定為處理器核數-1,如果是io密集型(很多網路請求),那麼就可以設定為2*處理器核數。但是這並不是一個銀彈,一切要從實際出發,最好就是在測試環境進行壓測,實踐出真知,並且很多時候一臺機器不止一個執行緒池或者還會有其他的執行緒,因此引數不可設定得太過飽滿。

一般 8 核的機器,設定 10-12 個核心執行緒就差不多了,這一切必須按照業務具體值進行計算。設定過多的執行緒數,上下文切換,競爭激烈,設定過少,沒有辦法充分利用計算機的資源。

計算(CPU)密集型消耗的主要是 CPU 資源,可以將執行緒數設定為 N(CPU 核心數)+1,比 CPU 核心數多出來的一個執行緒是為了防止執行緒偶發的缺頁中斷,或者其它原因導致的任務暫停而帶來的影響。一旦任務暫停,CPU 就會處於空閒狀態,而在這種情況下多出來的一個執行緒就可以充分利用 CPU 的空閒時間。

io密集型系統會用大部分的時間來處理 I/O 互動,而執行緒在處理 I/O 的時間段內不會佔用 CPU 來處理,這時就可以將 CPU 交出給其它執行緒使用。因此在 I/O 密集型任務的應用中,我們可以多配置一些執行緒,具體的計算方法是 2N。

為什麼不推薦預設的執行緒池建立方式?

阿里的程式設計規範裡面,不建議使用預設的方式來建立執行緒,是因為這樣建立出來的執行緒很多時候引數都是預設的,可能建立者不太瞭解,很容易出問題,最好通過new ThreadPoolExecutor()來建立,方便控制引數。預設的方式建立的問題如下:

  • Executors.newFixedThreadPool():無界佇列,記憶體可能被打爆
  • Executors.newSingleThreadExecutor():單個執行緒,效率低,序列。
  • Executors.newCachedThreadPool():沒有核心執行緒,最大執行緒數可能為無限大,記憶體可能還會爆掉。

使用具體的引數建立執行緒池,開發者必須瞭解每個引數的作用,不會胡亂設定引數,減少記憶體溢位等問題。

一般體現在幾個問題:

  • 任務佇列怎麼設定?
  • 核心執行緒多少個?
  • 最大執行緒數多少?
  • 怎麼拒絕任務?
  • 建立執行緒的時候沒有名稱,追溯問題不好找。

執行緒池的拒絕策略

執行緒池一般有以下四種拒絕策略,其實我們可以從它的內部類看出來:

  • AbortPolicy: 不執行新的任務,直接丟擲異常,提示執行緒池已滿
  • DisCardPolicy:不執行新的任務,但是也不會丟擲異常,默默的
  • DisCardOldSetPolicy:丟棄訊息佇列中最老的任務,變成新進來的任務
  • CallerRunsPolicy:直接呼叫當前的execute來執行任務

一般而言,上面的拒絕策略都不會特別理想,一般要是任務滿了,首先需要做的就是看任務是不是必要的,如果非必要,非核心,可以考慮拒絕掉,並報錯提醒,如果是必須的,必須把它儲存起來,不管是使用mq訊息,還是其他手段,不能丟任務。在這些過程中,日誌是非常必要的。既要保護執行緒池,也要對業務負責。

執行緒池監控與動態調整

執行緒池提供了一些API,可以動態獲取執行緒池的狀態,並且還可以設定執行緒池的引數,以及狀態:

檢視執行緒池的狀態:

修改執行緒池的狀態:

關於這一點,美團的執行緒池文章講得很清楚,甚至做了一個實時調整執行緒池引數的平臺,可以進行跟蹤監控,執行緒池活躍度、任務的執行Transaction(頻率、耗時)、Reject異常、執行緒池內部統計資訊等等。這裡我就不展開了,原文:https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html ,這是我們可以參考的思路。

執行緒池隔離

執行緒隔離,很多同學可能知道,就是不同的任務放在不同的執行緒裡面執行,而執行緒池隔離,一般是按照業務型別來隔離,比如訂單的處理執行緒放在一個執行緒池,會員相關的處理放在一個執行緒池。

也可以通過核心和非核心來隔離,核心處理流程放在一起,非核心放在一起,兩個使用不一樣的引數,不一樣的拒絕策略,儘量保證多個執行緒池之間不影響,並且最大可能保住核心執行緒的執行,非核心執行緒可以忍受失敗。

Hystrix裡面運用到這個技術,Hystrix的執行緒隔離技術,來防止不同的網路請求之間的雪崩,即使依賴的一個服務的執行緒池滿了,也不會影響到應用程式的其他部分。

關於作者

秦懷,公眾號【秦懷雜貨店】作者,技術之路不在一時,山高水長,縱使緩慢,馳而不息。個人寫作方向:Java原始碼解析,JDBC,Mybatis,Spring,redis,分散式,劍指Offer,LeetCode等,認真寫好每一篇文章,不喜歡標題黨,不喜歡花裡胡哨,大多寫系列文章,不能保證我寫的都完全正確,但是我保證所寫的均經過實踐或者查詢資料。遺漏或者錯誤之處,還望指正。

2020年我寫了什麼?

開源程式設計筆記

相關文章