深入併發-線程池

什麼是線程池

在 Java 中,如果每個請求到達就創建一個新線程,創建和銷燬線程花費的時間和消耗的系統資源都相當大,甚至可能要比在處理實際的用戶請求的時間和資源要多的多。
如果在一個 Jvm 裏創建太多的線程,可能會使系統由於過度消耗內存或“切換過度”而導致系統資源不足。
爲了解決這個問題,就有了線程池的概念,線程池的核心邏輯是提前創建好若干個線程放在一個容器中。如果有任務需要處理,則將任務直接分配給線程池中的線程來執行就行,任務處理完以後這個線程不會被銷燬,而是等待後續分配任務。同時通過線程池來重複管理線程還可以避免創建大量線程增加開銷。

線程池的優勢

合理的使用線程池,可以帶來一些好處

  1. 降低創建線程和銷燬線程的性能開銷
  2. 提高響應速度,當有新任務需要執行是不需要等待線程創建就可以立馬執行
  3. 合理的設置線程池大小可以避免因爲線程數超過硬件資源瓶頸帶來的問題

線程池 API

在 Executors 裏面提供了幾個線程池的工廠方法,這樣,很多新手就不需要了解太多關於 ThreadPoolExecutor 的知識了,他們只需要直接使用 Executors 的工廠方法,就可以使用線程池:

  • newCachedThreadPool:返回一個可根據實際情況調整線程個數的線程池,不限制最大線程數量,若用空閒的線程則執行任務,若無任務則不創建線程。並且每一個空閒線程會在 60 秒後自動回收
  • newSingleThreadExecutor: 創建一個線程的線程池,若空閒則執行,若沒有空閒線程則暫緩在任務隊列中。
  • newFixedThreadPool:該方法返回一個固定數量的線程池,線程數不變,當有一個任務提交時,若線程池中空閒,則立即執行;若沒有則會被暫緩在一個任務隊列中,等待有空閒的線程去執行。
  • newScheduledThreadPool: 創建一個可以指定線程的數量的線程池,但是這個線程池還帶有延遲和週期性執行任務的功能,類似定時器。
ExecutorService executor = Executors.newCachedThreadPool();
ExecutorService executor = Executors.newSingleThreadExecutor();
ExecutorService executor = Executors.newFixedThreadPool(3);
ExecutorService executor = Executors.newScheduledThreadPool(1);

Executors 的工廠方法也都是根據 ThreadPoolExecutor 進行創建的,我們來看下 ThreadPoolExecutor 的構造參數:

參數名 作用
corePoolSize 核心線程池大小
maximumPoolSize 最大線程池大小
keepAliveTime 線程池中超過corePoolSize數目的空閒線程最大存活時間;可以allowCoreThreadTimeOut(true)使得核心線程有效時間
TimeUnit keepAliveTime時間單位
workQueue 阻塞任務隊列
ThreadFactory 新建線程工廠
RejectedExecutionHandler 當提交任務數超過maxmumPoolSize+workQueue之和時,任務會交給RejectedExecutionHandler來處理
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

實現原理

線程池的基本使用我們都清楚了,接下來我們來了解一下線程池的實現原理

  1. 當線程池小於corePoolSize時,新提交任務將創建一個新線程執行任務,即使此時線程池中存在空閒線程
  2. 當線程池達到corePoolSize時,新提交任務將被放入workQueue中,等待線程池中任務調度執行
  3. 當workQueue已滿,且maximumPoolSize>corePoolSize時,新提交任務會創建新線程執行任務
  4. 當提交任務數超過maximumPoolSize時,新提交任務由RejectedExecutionHandler處理
  5. 當線程池中超過corePoolSize線程,workQueue沒有任務導致空閒時間達到keepAliveTime時,關閉空閒線程
  6. 當設置allowCoreThreadTimeOut(true)時,線程池中corePoolSize線程空閒時間達到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)
            addWorker(null, false);
    }
    //核心池已滿,隊列已滿,試着創建一個新線程
    else if (!addWorker(command, false))
    	//如果創建新線程失敗了,說明線程池被關閉或者線程池完全滿了,拒絕任務
        reject(command);
}

看一下線程池定義的幾種狀態

private static final int RUNNING = -1 << COUNT_BITS;// 接收新任務,並執行隊列中的任務
private static final int SHUTDOWN = 0 << COUNT_BITS;// 不接收新任務,但是執行隊列中的任務
private static final int STOP = 1 << COUNT_BITS;// 不接收新任務,不執行隊列中的任務,中斷正在執行中的任務
private static final int TIDYING = 2 << COUNT_BITS; //所有的任務都已結束,線程數量爲 0,處於該狀態的線程池即將調用 terminated()方法
private static final int TERMINATED = 3 << COUNT_BITS;// terminated()方法執行完成

在這裏插入圖片描述
如果工作線程數小於核心線程數的話,會調用 addWorker,顧名思義,其實就是要創建一個
工作線程。我們來看看源碼的實現,主要就做了兩件事:

  1. 採用循環 CAS 操作來將線程數加 1
  2. 新建一個線程並啓用
private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // 1. 如果線程處於非運行狀態,拒絕添加新的任務
        // 2. SHUTDOWN 狀態線程池不接受新任務,但仍然會執行已經加入任務隊列的任務。
        //所以當進入 SHUTDOWN 狀態,而傳進來的任務爲空,並且任務隊列不爲空的時候,
        //是允許添加新線程的,如果把這個條件取反,就表示不允許添加 worker
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;

        for (;;) {
        	//獲得 Worker 工作線程數
            int wc = workerCountOf(c);
            //如果工作線程數大於默認容量大小或者大於核心線程數大小,則直接返回 false 表示不能再添加 worker
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            //通過 cas 來增加工作線程數,如果 cas 失敗,則直接重試
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  
            //如果線程池狀態發送了變化,則繼續重試
            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 {
    	//將任務傳入構建一個 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());
				//要麼線程池處於運行中狀態,要麼處於 SHUTDOWN 狀態並且當前任務爲空才能添加 Worker 集合中
                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 {
    	//添加失敗則需要將 work 數量減回去
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

Worker 類繼承了 AQS,並實現了 Runnable 接口,注意其中的 firstTask 和 thread 屬性:firstTask 用它來保存傳入的任務;thread 是在調用構造方法時通過 ThreadFactory 來創建的線程,是用來處理任務的線程。
在調用構造方法時,需要傳入任務,這裏通過 getThreadFactory().newThread(this);來新建一個線程,newThread 方法傳入的參數是 this,因爲 Worker 本身繼承了 Runnable 接口,也就是一個線程,所以一個 Worker 對象在啓動的時候會調用 Worker 類中的 run 方法。

Worker(Runnable firstTask) {
    setState(-1); 
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);
}

看下 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;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
    	// 如果 task 爲空,則通過getTask 來獲取任務
        while (task != null || (task = getTask()) != null) {
         	//上鎖,不是爲了防止併發執行任務,爲了在 shutdown()時不終止正在運行的 worker
            w.lock();
           // 線程池爲 stop 狀態時不接受新任務,不執行已經加入任務隊列的任務,還中斷正在執行的任務
            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 {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
    	//1.將入參 worker 從數組 workers 裏刪除掉;
		//2.根據布爾值 allowCoreThreadTimeOut 來決定是否補充新的 Worker 進數組 workers
        processWorkerExit(w, completedAbruptly);
    }
}

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

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,此時不用考慮 workQueue的情況
		**/
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);

        // 如果當前線程池需要進行核心線程超時銷燬或線程數量大於核心線程數量,則需要進行超時控制
        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)
                return r;
            //如果 r==null,說明已經超時了,設置 timedOut=true,在下次自旋的時候減少線程數量
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

什麼時候會銷燬線程呢?當然是 runWorker 方法執行完之後,也就是 Worker 中的 run 方法執行完調用 processWorkerExit 方法進行銷燬線程,最後由 JVM 自動回收

拒絕策略

線程池的拒絕策略有四種,根據業務需求自選也可以自己定義:

  1. AbortPolicy:該策略是線程池的默認策略。使用該策略時,如果線程池隊列滿了丟掉這個任務並且拋出RejectedExecutionException異常。
  2. DiscardPolicy:這個策略和AbortPolicy的slient版本,如果線程池隊列滿了,會直接丟掉這個任務並且不會有任何異常。
  3. DiscardOldestPolicy:這個策略從字面上也很好理解,丟棄最老的。也就是說如果隊列滿了,會將最早進入隊列的任務刪掉騰出空間,再嘗試加入隊列。因爲隊列是隊尾進,隊頭出,所以隊頭元素是最老的,因此每次都是移除對頭元素後再嘗試入隊。
  4. CallerRunsPolicy:使用此策略,如果添加到線程池失敗,那麼主線程會自己去執行該任務,不會等待線程池中的線程去執行。就像是個急脾氣的人,我等不到別人來做這件事就乾脆自己幹。

注意事項

如何合理配置線程池的大小

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

如果是 CPU 密集型,主要是執行計算任務,響應時間很快,cpu 一直在運行。這種任務 cpu 的利用率很高,那麼線程數的配置應該根據 CPU 核心數來決定,CPU 核心數=最大同時執行線程數。過多的線程會導致上下文切換反而使得效率降低,那線程池的最大線程數可以配置爲 cpu 核心數+1。
如果是 IO 密集型,主要是進行 IO 操作,執行 IO 操作的時間較長。這時 cpu 處於空閒狀態,導致 cpu 的利用率不高,這種情況下可以增加線程池的大小。這種情況下可以結合線程的等待時長來做判斷,等待時間越高,那麼線程數也相對越多。一般可以配置 cpu 核心數的 2 倍。一個公式:線程池設定最佳線程數目 = ((線程池設定的線程等待時間+線程 CPU 時間)/ 線程 CPU 時間 )* CPU 數目。這個公式的線程 cpu 時間是預估的程序單個線程在 cpu 上運行的時間(通常使用 loadrunner測試大量運行次數求出平均值)

線程池中的線程初始化

默認情況下,創建線程池之後,線程池中是沒有線程的,需要提交任務之後纔會創建線程。
在實際中如果需要線程池創建之後立即創建線 程,可以通過以下兩個方法辦到:

  • prestartCoreThread():初始化一個核心線程
  • prestartAllCoreThreads():初始化所有核心線程

線程池關閉

ThreadPoolExecutor 提供了兩個方法,用於線程池的關閉:

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

線程池容量的動態調整

ThreadPoolExecutor 提供了動態調整線程池容量大小的方法:

  • setCorePoolSize:設置核心池大小
  • setMaximumPoolSize:設置線程池最大能創建的線程數目大小

任務緩存隊列及排隊策略

在前面我們多次提到了任務緩存隊列,即 workQueue,它用來存放等待執行的任務。workQueue 的類型爲 BlockingQueue,通常可以取下面三種類型:

  1. ArrayBlockingQueue:基於數組的先進先出隊列,此隊列創建時必須指定大小;
  2. LinkedBlockingQueue:基於鏈表的先進先出隊列,如果創建時沒有指定此隊列大小,則默認爲 Integer.MAX_VALUE;
  3. SynchronousQueue:這個隊列比較特殊,它不會保存提交的任務,而是將直接新建一個
    線程來執行新來的任務;

線程池的監控

如果在項目中大規模的使用了線程池,那麼必須要有一套監控體系,來指導當前線程池的狀態,當出現問題的時候可以快速定位到問題。而線程池提供了相應的擴展方法,我們通過重寫線程池的 beforeExecute、afterExecute 和 shutdown 等方式就可以實現對線程的監控。

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