多線程(7)線程池(參數解析以及源碼解析)

一、爲什麼使用線程池?

構建一個新的線程是有一定代價的,因爲涉及與操作系統的交互。如果程序中創建了大量的生命期很短的線程,應該使用線程池。一個線程池中包含許多準備運行的空閒線程。將Runnable對象交給線程池,就會有一個線程調用run方法。當run方法退出時,線程不會死亡,而是在池中準備爲下一個請求提供服務。

另一個使用線程池的理由是減少併發線程的數目。創建大量線程會大大降低性能甚至使虛擬機崩潰。如果有一個會創建許多線程的算法,應該使用一個線程數“固定的”線程池以限制併發線程的總數。

線程池的好處

1、降低資源消耗。重用線程池中的線程,避免因頻繁創建和銷燬線程造成的性能消耗。
2、提高響應速度。當任務到達時,任務可以不需要等到線程創建就能執行。
3、對線程進行有效的管理。使用線程池可以對線程進行統一的分配,調優和監控。

二、線程池的創建

線程池的創建需要通過ThreadPoolExecutor,可以通過ThreadPoolExecutor的構造函數創建,也可以利用執行器創建。
線程池的創建:參考文章的執行器部分

JDK中線程池的核心實現類是ThreadPoolExecutor,線程池的創建需要通過ThreadPoolExecutor。
newCachedThreadPool、newFixedThreadPool、newSingleThreadExecutor這 3 個靜態工廠方法返回實現了ExecutorService 接口的 ThreadPoolExecutor 類的對象。例如:

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

三、線程池的參數

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

可以看到,線程池的構造函數裏有7個參數,下面分別介紹這些參數的含義。

corePoolSize

核心線程數。

在創建線程池之後,默認情況下線程池中並沒有任何的線程,而是等待任務到來才創建線程去執行任務,當線程池中的線程數目達到corePoolSize後,新來的任務將會被添加到緩存隊列中,也就是那個workQueue。

maximumPoolSize

最大線程數。

表示線程池中最多可以創建多少個線程,很多人以爲它的作用是這樣的:”當線程池中的任務數超過 corePoolSize 後,線程池會繼續創建線程,直到線程池中的線程數小於maximumPoolSize“,其實這種理解是完全錯誤的。它真正的作用是:當線程池中的線程數等於 corePoolSize 並且 workQueue 已滿,這時就要看當前線程數是否大於 maximumPoolSize,如果小於maximumPoolSize 定義的值,則會繼續創建線程去執行任務, 否則將會調用去相應的任務拒絕策略來拒絕這個任務。另外超過 corePoolSize的線程被稱做"Idle Thread", 這部分線程會有一個最大空閒存活時間(keepAliveTime),如果超過這個空閒存活時間還沒有任務被分配,則會將這部分線程進行回收。

workQueue

工作隊列。

阻塞隊列,用來存儲等待執行的任務,決定了線程池的排隊策略。當新任務來的時候,先判斷當前運行的線程數是否達到了corePoolSize,如果達到了,新任務就會被放入隊列中。

Java中常用的阻塞隊列:阻塞隊列

keepAliveTime

存活時間。

一個線程如果處於空閒狀態,並且當前的線程數量大於corePoolSize,那麼在指定時間後,這個空閒線程會被銷燬,這裏的指定時間由keepAliveTime來設定。

unit

keepAliveTime的計量單位。

是一個枚舉類型。 有NANOSECONDS, MICROSECONDS, MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS,7個可選值。

threadFactory

線程工廠。

創建一個新線程時使用的工廠,可以用來設定線程名、是否爲daemon線程等等。當我們不指定線程工廠時,線程池內部會調用Executors.defaultThreadFactory()創建默認的線程工廠,其後續創建的線程優先級都是Thread.NORM_PRIORITY。如果我們指定線程工廠,我們可以對產生的線程進行一定的操作。

handler

拒絕策略。

當線程數量已經達到最大線程數量,並且隊列已滿的情況下,如果有新任務來就會執行拒絕策略。

拒絕策略有以下幾種:

  1. AbortPolicy(默認策略)

該策略下,直接丟棄任務,並拋出RejectedExecutionException異常

  1. CallerRunsPolicy

該策略下,在調用者線程中直接執行被拒絕任務的run方法,除非線程池已經shutdown,則直接拋棄任務。(會降低對於新任務的提交速度,影響程序的整體性能)

  1. DiscardPolicy

該策略下,直接丟棄任務,什麼都不做。

  1. DiscardOldestPolicy

該策略下,拋棄進入隊列最早的那個任務,然後嘗試把這次拒絕的任務放入隊列

線程池工作流程

在這裏插入圖片描述

四、線程池原理(源碼解析)

ThreadPoolExecutor 是線程池的核心,提供了線程池的實現。前面說過,newFixedThreadPool等方法最後會返回一個ThreadPoolExecutor的對象,以newFixedThreadPool爲例,可以先看下它的流程圖:
在這裏插入圖片描述
具體的代碼會在下面分析。

ThreadPoolExecutor

首先看下ThreadPoolExecutor的部分源碼:

這個類的第一個成員變量ctl,AtomicInteger這個類可以通過CAS達到無鎖併發,效率比較高,這個變量有雙重身份,它的高3位表示線程池的狀態(狀態有5種,所以需要3位),低29位表示線程池中現有的線程數,這也是Doug Lea一個天才的設計,用最少的變量來減少鎖競爭,提高併發效率。

    //CAS,無鎖併發
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    //表示線程池線程數的bit數
    private static final int COUNT_BITS = Integer.SIZE - 3;
    //最大的線程數量,數量是完全夠用了
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

    // runState is stored in the high-order bits
    //1110 0000 0000 0000 0000 0000 0000 0000(很耿直的我)
    private static final int RUNNING    = -1 << COUNT_BITS;
    //0000 0000 0000 0000 0000 0000 0000 0000(很耿直的我)
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    //0010 0000 0000 0000 0000 0000 0000 0000(很耿直的我)
    private static final int STOP       =  1 << COUNT_BITS;
    //0100 0000 0000 0000 0000 0000 0000 0000(很耿直的我)
    private static final int TIDYING    =  2 << COUNT_BITS;
    //0110 0000 0000 0000 0000 0000 0000 0000(很耿直的我)
    private static final int TERMINATED =  3 << COUNT_BITS;

    // Packing and unpacking ctl
    //獲取線程池的狀態
    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; }

    /*
     * Bit field accessors that don't require unpacking ctl.
     * These depend on the bit layout and on workerCount being never negative.
     * 判斷狀態c是否比s小,下面會給出狀態流轉圖
     */
    
    private static boolean runStateLessThan(int c, int s) {
        return c < s;
    }
    
    //判斷狀態c是否不小於狀態s
    private static boolean runStateAtLeast(int c, int s) {
        return c >= s;
    }
    //判斷線程是否在運行
    private static boolean isRunning(int c) {
        return c < SHUTDOWN;
    }

線程池的五種狀態

  1. RUNNING - 接受新任務並且繼續處理阻塞隊列中的任務
  2. SHUTDOWN - 不接受新任務但是會繼續處理阻塞隊列中的任務
  3. STOP - 不接受新任務,不在執行阻塞隊列中的任務,中斷正在執行的任務
  4. TIDYING - 所有任務都已經完成,線程數都被回收,線程會轉到TIDYING狀態會繼續執行鉤子方法
  5. TERMINATED - 鉤子方法執行完畢

狀態轉化如下:
在這裏插入圖片描述

execute/submit

向線程池提交任務有這2種方式,execute是ExecutorService接口定義的,submit有三種方法重載都在AbstractExecutorService中定義,都是將要執行的任務包裝爲FutureTask來提交,使用者可以通過FutureTask來拿到任務的執行狀態和執行最終的結果,最終調用的都是execute方法,其實對於線程池來說,它並不關心你是哪種方式提交的,因爲任務的狀態是由FutureTask自己維護的,對線程池透明。

看下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)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

三步操作爲:

  1. 檢查當前線程池中的線程數是否<核心線程數,如果小於核心線程數,就調用addWorker方法創建一個新的線程執行任務,addworker中的第二個參數傳入true,表示當前創建的是核心線程。如果當前線程數>=核心線程數或者創建線程失敗的話,直接進入第二種情況。

  2. 通過調用isRunning方法判斷線程池是否還在運行,如果線程池狀態不是running,那就直接退出execute方法,沒有執行的必要了;如果線程池的狀態是running,嘗試着把任務加入到queue中,再次檢查線程池的狀態, 如果當前不是running,可能在入隊後調用了shutdown方法,所以要在queue中移除該任務,默認採用拒絕策略直接拋出異常。如果當前線程數爲0,可能把allowCoreThreadTimeOut設爲了true,正好核心線程全部被回收,所以必須要創建一個空的線程,讓它自己去queue中去取任務執行。

  3. 如果當前線程數>核心線程數,並且入隊失敗,調用addWorker方法創建一個新的線程去執行任務,第二個參數是false,表示當前創建的線程不是核心線程。這種情況表示核心線程已滿並且queue已滿,如果當前線程數小於最大線程數,創建線程執行任務。如果當前線程數>=最大線程數,默認直接採取拒絕策略。

addWorker

addWorker,顧名思義,其實就是要創建一個工作線程。源碼比較長,其實就做了兩件事:

  • 採用循環 CAS 操作來將線程數加 1;
  • 新建一個線程並啓用。
    private boolean addWorker(Runnable firstTask, boolean core) {
        retry: //goto 語句,避免死循環
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);
            // Check if queue empty only if necessary.
            /*如果線程處於非運行狀態,並且 rs 不等於 SHUTDOWN 且 firstTask 不等於空且且
            workQueue 爲空,直接返回 false(表示不可添加 work 狀態)
            1. 線程池已經 shutdown 後,還要添加新的任務,拒絕
            2. (第二個判斷)SHUTDOWN 狀態不接受新任務,但仍然會執行已經加入任務隊列的任
            務,所以當進入 SHUTDOWN 狀態,而傳進來的任務爲空,並且任務隊列不爲空的時候,是允許添加
            新線程的,如果把這個條件取反,就表示不允許添加 worker*/
            if (rs >= SHUTDOWN &&
                    ! (rs == SHUTDOWN &&
                            firstTask == null &&
                            ! workQueue.isEmpty()))
                return false;
            for (;;) { //自旋
                int wc = workerCountOf(c);//獲得 Worker 工作線程數
                //如果工作線程數大於默認容量大小或者大於核心線程數大小,則直接返回 false 表示不
                能再添加 worker。
                if (wc >= CAPACITY ||
                        wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                if (compareAndIncrementWorkerCount(c))//通過 cas 來增加工作線程數,如果 cas 失敗,則直接重試
                    break retry;
                c = ctl.get(); // Re-read ctl //再次獲取 ctl 的值
                if (runStateOf(c) != rs) //這裏如果不想等,說明線程的狀態發生了變化,繼續重試
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }

        
        //上面這段代碼主要是對 worker 數量做原子+1 操作,下面的邏輯纔是正式構建一個 worker


        boolean workerStarted = false; //工作線程是否啓動的標識
        boolean workerAdded = false; //工作線程是否已經添加成功的標識
        Worker w = null;
        try {
            w = new Worker(firstTask); //構建一個 Worker,這個 worker 是什麼呢?我們可以看到構造方法裏面傳入了一個 Runnable 對象
            final Thread t = w.thread; //從 worker 對象中取出線程
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock(); //這裏有個重入鎖,避免併發問題
                try {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    int rs = runStateOf(ctl.get());
                    //只有當前線程池是正在運行狀態,[或是 SHUTDOWN 且 firstTask 爲空],才能添加到 workers 集合中
                    if (rs < SHUTDOWN ||
                            (rs == SHUTDOWN && firstTask == null)) {
                        //任務剛封裝到 work 裏面,還沒 start,你封裝的線程就是 alive,幾個意思?肯定是要拋異常出去的
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        workers.add(w); //將新創建的 Worker 添加到 workers 集合中
                        int s = workers.size();
                        //如果集合中的工作線程數大於最大線程數,這個最大線程數表示線程池曾經出現過的最大線程數
                        if (s > largestPoolSize)
                            largestPoolSize = s; //更新線程池出現過的最大線程數
                        workerAdded = true;//表示工作線程創建成功了
                    }
                } finally {
                    mainLock.unlock(); //釋放鎖
                }
                if (workerAdded) {//如果 worker 添加成功
                    t.start();//啓動線程
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w); //如果添加失敗,就需要做一件事,就是遞減實際工作線程數(還記得我們最開始的時候增加了工作線程數嗎)
        }
        return workerStarted;//返回結果
    }

把worker對象放入hashset中,hashset的底層就是hashmap實現的,hashmap是線程不安全的,所以必須要加鎖。workers就是當前的工作線程。
private final HashSet<Worker> workers = new HashSet<Worker>();

從代碼種可以看到,execute方法雖然沒有加鎖,但是在addWorker方法內部加鎖了,這樣可以保證不會創建超過我們預期的線程數,大師在設計的時候,做到了在最小的範圍內加鎖,儘量減少鎖競爭。
core參數,只是用來判斷當前線程數是否超量的時候跟corePoolSize還是maxPoolSize比較,Worker本身無核心或者非核心的概念。

Worker

addWorker 方法只是構造了一個 Worker,並且把 firstTask 封裝到 worker 中,下面看下worker的源碼:

private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
        private static final long serialVersionUID = 6138294804551838833L;
        /**
         * Thread this worker is running in. Null if factory fails.
         */
        final Thread thread; //注意了,這纔是真正執行 task 的線程,從構造函數可知是由ThreadFactury 創建的
        /**
         * Initial task to run. Possibly null.
         */
        Runnable firstTask; //這就是需要執行的 task
        /**
         * Per-thread task counter
         */
        volatile long completedTasks; //完成的任務數,用於線程池統計

        Worker(Runnable firstTask) {
            setState(-1); //初始狀態 -1,防止在調用 runWorker(),也就是真正執行 task前中斷 thread。
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }

        public void run() {
            runWorker(this);
        }

        ...
    }
  1. 每個 worker,都是一條線程,同時裏面包含了一個 firstTask,即初始化時要被首先執行的任務。
  2. 最終執行任務的,是 runWorker()方法。

Worker 類繼承了 AQS,並實現了 Runnable 接口,注意其中的 firstTask 和 thread 屬性:firstTask 用它來保存傳入的任務;thread 是在調用構造方法時通過 ThreadFactory 來創建的線程,是用來處理任務的線程。

在調用構造方法時,需要傳入任務,這裏通過 getThreadFactory().newThread(this);來新建一個線程,newThread 方法傳入的參數是 this,因爲 Worker 本身繼承了 Runnable 接口,也就是一個線程,所以一個 Worker 對象在啓動的時候會調用 Worker 類中的 run 方法。

Worker 繼承了 AQS,使用 AQS 來實現獨佔鎖的功能。爲什麼不使用 ReentrantLock 來實現呢?可以看到 tryAcquire 方法,它是不允許重入的,而 ReentrantLock 是允許重入的

爲什麼要使用獨佔鎖:lock 方法一旦獲取了獨佔鎖,表示當前線程正在執行任務中。那麼它會有以下幾個作用:

  1. 如果正在執行任務,則不應該中斷線程;
  2. 如果該線程現在不是獨佔鎖的狀態,也就是空閒的狀態,說明它沒有在處理任務,這時可以對該線程進行中斷;
  3. 線程池在執行 shutdown 方法或 tryTerminate 方法時會調用 interruptIdleWorkers 方法來中斷空閒的線程,interruptIdleWorkers 方法會使用 tryLock 方法來判斷線程池中的線程是否是空閒狀態
  4. 之所以設置爲不可重入,是因爲我們不希望任務在調用像 setCorePoolSize 這樣的線程池控制方法時重新獲取鎖,這樣會中斷正在運行的線程

addWorkerFailed

addWorker 方法中,如果添加 Worker 並且啓動線程失敗,則會做失敗後的處理:

    private void addWorkerFailed(Worker w) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            if (w != null)
                workers.remove(w);
            decrementWorkerCount();
            tryTerminate();
        } finally {
            mainLock.unlock();
        }
    }

主要做三件事:

  1. 如果 worker 已經構造好了,則從 workers 集合中移除這個 worker
  2. 原子遞減核心線程數(因爲在 addWorker 方法中先做了原子增加)
  3. 嘗試結束線程池

runWorker

前面已經瞭解了 ThreadPoolExecutor 的核心方法 addWorker,主要作用是增加工作線程,而 Worker 簡單理解其實就是一個線程,裏面重新了 run 方法,這塊是線程池中執行任務的真正處理邏輯,也就是 runWorker 方法,這個方法主要做幾件事:

  1. 如果 task 不爲空,則開始執行 task
  2. 如果 task 爲空,則通過 getTask()再去取任務,並賦值給 task,如果取到的 Runnable 不爲空,則執行該任務
  3. 執行完畢後,通過 while 循環繼續 getTask()取任務
  4. 如果 getTask()取到的任務依然是空,那麼整個 runWorker()方法執行完畢
final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        //unlock,表示當前 worker 線程允許中斷,因爲 new Worker 默認的 state=-1,此處是調用
        //Worker 類的 tryRelease()方法,將 state 置爲 0,
        //而 interruptIfStarted()中只有 state>=0 才允許調用中斷
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            //注意這個 while 循環,在這裏實現了 [線程複用] // 如果 task 爲空,則通過getTask 來獲取任務
            while (task != null || (task = getTask()) != null) {
                w.lock(); //上鎖,不是爲了防止併發執行任務,爲了在 shutdown()時不終止正在運行的 worker
                //線程池爲 stop 狀態時不接受新任務,不執行已經加入任務隊列的任務,還中斷正在執行的任務
                //所以對於 stop 狀態以上是要中斷線程的
                //(Thread.interrupted() &&runStateAtLeast(ctl.get(), STOP)確保線程中斷標誌位爲 true 且是 stop 狀態以上,接着清除了中斷標誌
                //!wt.isInterrupted()則再一次檢查保證線程需要設置中斷標誌位
                if ((runStateAtLeast(ctl.get(), STOP) ||
                        (Thread.interrupted() &&
                                runStateAtLeast(ctl.get(), STOP))) &&
                        !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);//這裏默認是沒有實現的,在一些特定的場景中我們可以自己繼承 ThreadpoolExecutor 自己重寫
                    Throwable thrown = null;
                    try {
                        task.run(); //執行任務中的 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 {
                    //置空任務(這樣下次循環開始時,task 依然爲 null,需要再通過 getTask()取) + 記錄該 Worker 完成任務數量 + 解鎖
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
            //1.將入參 worker 從數組 workers 裏刪除掉;
            //2.根據布爾值 allowCoreThreadTimeOut 來決定是否補充新的 Worker 進數組workers
        }
    }

首先還是獲取當前線程,獲取當前worker對象中的任務task,把當前線程的狀態由-1設爲0,表示可以獲取鎖執行任務,接下來就是一個while循環(這個循環裏實現了線程複用),當task不爲空或者從gettask方法取出的任務不爲空的時候,加鎖,底層還是使用了AQS,保證了只有一個線程執行完畢其他線程才能執行。在執行任務之前,必須進行判斷,線程池的狀態如果>=STOP,必須中斷當前線程,如果是running或者shutdown,當前線程不能被中斷,防止線程池調用了shutdownnow方法必須中斷所有的線程。

在處理任務之前,會執行beforeExecute方法, 在處理任務之後,執行afterExecute方法,這兩個都是鉤子方法,繼承了ThreadPoolExecutor可以重寫此方法,嵌入自定義的邏輯。一旦在任務運行的過程中,出現異常會直接拋出,所以在實際的業務中,應該使用try…catch,把這些日常加入到日誌中。

任務執行完,就把task設爲空,累加當前線程完成的任務數,unlock,繼續從queue中取任務執行。

getTask

worker 線程會從阻塞隊列中獲取需要執行的任務,這個方法不是簡單的 take 數據,我們來分析下他的源碼實現:

你也許好奇是怎樣判斷線程有多久沒有活動了,是不是以爲線程池會啓動一個監控線程,專門監控哪個線程正在偷懶?想太多,其實只是在線程從工作隊列 poll 任務時,加上了超時限制,如果線程在 keepAliveTime 的時間內 poll 不到任務,那我就認爲這條線程沒事做,可以幹掉了,看看這個代碼片段你就清楚了

private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?
        for (;;) {//自旋
            int c = ctl.get();
            int rs = runStateOf(c);
            /* 對線程池狀態的判斷,兩種情況會 workerCount-1,並且返回 null
            1. 線程池狀態爲 shutdown,且 workQueue 爲空(反映了 shutdown 狀態的線程池還是要執行 workQueue 中剩餘的任務的)
            2. 線程池狀態爲 stop(shutdownNow()會導致變成 STOP)(此時不用考慮 workQueue的情況)*/
            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;//返回 null,則當前 worker 線程會退出
            }
            int wc = workerCountOf(c);
            // timed 變量用於判斷是否需要進行超時控制。
            // allowCoreThreadTimeOut 默認是 false,也就是核心線程不允許進行超時;
            // wc > corePoolSize,表示當前線程池中的線程數量大於核心線程數量;
            // 對於超過核心線程數量的這些線程,需要進行超時控制
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
            /*1. 線程數量超過 maximumPoolSize 可能是線程池在運行時被調用了 setMaximumPoolSize()
            被改變了大小,否則已經 addWorker()成功不會超過 maximumPoolSize
            2. timed && timedOut 如果爲 true,表示當前操作需要進行超時控制,並且上次從阻塞隊列中
            獲取任務發生了超時.其實就是體現了空閒線程的存活時間*/
            if ((wc > maximumPoolSize || (timed && timedOut))
                    && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }
            try {
                /*根據 timed 來判斷,如果爲 true,則通過阻塞隊列 poll 方法進行超時控制,如果在
                keepaliveTime 時間內沒有獲取到任務,則返回 null.
                否則通過 take 方法阻塞式獲取隊列中的任務*/
                Runnable r = timed ?
                        workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                        workQueue.take();
                if (r != null)//如果拿到的任務不爲空,則直接返回給 worker 進行處理
                    return r;
                timedOut = true;//如果 r==null,說明已經超時了,設置 timedOut=true,在下次自旋的時候進行回收
            } catch (InterruptedException retry) {
                timedOut = false;// 如果獲取任務時當前線程發生了中斷,則設置 timedOut 爲false 並返回循環重試
            }
        }
    }

這裏重要的地方是第二個 if 判斷,目的是控制線程池的有效線程數量。由上文中的分析可以知道,在執行 execute 方法時,如果當前線程池的線程數量超過了 corePoolSize 且小於maximumPoolSize,並且 workQueue 已滿時,則可以增加工作線程,但這時如果超時沒有獲取到任務,也就是 timedOut 爲 true 的情況,說明 workQueue 已經爲空了,也就說明了當前線程池中不需要那麼多線程來執行任務了,可以把多於 corePoolSize 數量的線程銷燬掉,保持線程數量在 corePoolSize 即可

什麼時候會銷燬?getTask 方法返回 null 時,在 runWorker 方法中會跳出 while 循環,然後會執行processWorkerExit 方法。processWorkerExit 方法會將這個worker從workers中移出。之後由JVM自動回收。

shutdown/shutdownNow

ThreadPoolExecutor 提 供 了 兩 個 方 法 , 用 於 線 程 池 的 關 閉 , 分 別 是 shutdown() 和shutdownNow(),其中:

  • shutdown():不會立即終止線程池,而是要等所有任務緩存隊列中的任務都執行完後才終止,但再也不會接受新的任務
  • shutdownNow():立即終止線程池,並嘗試打斷正在執行的任務,並且清空任務緩存隊列,返回尚未執行的任務

五、配置線程池的大小

如何合理配置線程池大小,線程池大小不是靠猜,也不是說越多越好。
在遇到這類問題時,先冷靜下來分析

  1. 需要分析線程池執行的任務的特性: CPU 密集型還是 IO 密集型
  2. 每個任務執行的平均時長大概是多少,這個任務的執行時長可能還跟任務處理邏輯是否涉及到網絡傳輸以及底層系統資源依賴有關係

如果是 CPU 密集型,主要是執行計算任務,響應時間很快,cpu 一直在運行,這種任務 cpu的利用率很高,那麼線程數的配置應該根據 CPU 核心數來決定,CPU 核心數=最大同時執行線程數,假如 CPU 核心數爲 4,那麼服務器最多能同時執行 4 個線程。過多的線程會導致上下文切換反而使得效率降低。那線程池的最大線程數可以配置爲 cpu 核心數+1。
如果是 IO 密集型,主要是進行 IO 操作,執行 IO 操作的時間較長,這是 cpu 出於空閒狀態,導致 cpu 的利用率不高,這種情況下可以增加線程池的大小。這種情況下可以結合線程的等待時長來做判斷,等待時間越高,那麼線程數也相對越多。一般可以配置 cpu 核心數的 2 倍

一個公式:線程池設定最佳線程數目 = ((線程池設定的線程等待時間+線程 CPU 時間)/ 線程 CPU 時間 )* CPU 數目

這個公式的線程 cpu 時間是預估的程序單個線程在 cpu 上運行的時間(通常使用 loadrunner測試大量運行次數求出平均值)

參考鏈接:Java線程池七個參數詳解
參考鏈接:ThreadPoolExecutor源碼解析
參考鏈接:徹底理解Java線程池原理篇
參考鏈接:ThreadPoolExecutor源碼剖析

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