詳解ThreadPoolExecutor線程池的原理

使用方法

這裏我們用最簡單的形式來創建一個線程池,目的是先演示一下使用ThreadPoolExecutor的使用方法

    public static void main(String[] args) {
    	// 創建線程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 10, 200, TimeUnit.SECONDS, new LinkedBlockingQueue<>(5));

        for (int i = 0; i < 10; i++) {
        	// 使用線程池
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread() + " -> run");
                }
            };
        }
        // 銷燬線程池
        threadPool.shutdown();
    }

就這麼簡單,一個線程池就演示完畢了,包含了創建、使用和銷燬的全過程

概念講解

爲了接下來的源碼分析做準備,這裏先來詳解一些與ThreadPoolExecutor線程池有關的一些概念

參數

ThreadPoolExecutor的構造方法中需要傳入很多參數,這些參數的含義也十分重要,希望大家能記住,下面按順序列出構造方法中的參數:

  • corePoolSize:核心線程數,在不主動設置的情況下,核心線程是不會自動銷燬的,超過了核心線程數的線程會根據設置的等待時間自動銷燬
  • maximumPoolSize:最大線程數,線程池可容納的最大線程數
  • keepAliveTime:空閒線程(非核心線程)在等待keepAliveTime時間後會自動銷燬
  • unit:keepAliveTime的時間單位
  • workQueue:任務隊列,如果線程數量超過核心線程數,則新來的任務會先進入這個隊列中,然後再做下一步判斷,用於緩衝
  • threadFactory(可選):線程工廠,用於設定創建線程的方式,一般用來設置線程名
  • handler(可選):拒絕策略,當隊列滿或線程池滿時,類中提供了四種拒絕策略

裏面可講的不多,值得一提的是workQueue是一個BlockingQueue類型的對象,一般選用ArrayBlockingQueue或LinkedBlockingQueue

還有就是handler在ThreadPoolExecutor中有以下四種可選參數:

  • AbortPolicy(默認):丟棄任務並拋出異常
  • CallerRunsPolicy:讓調用execute方法的線程來執行該任務
  • DiscardPolicy:丟棄任務,不拋出異常
  • DiscardOldestPolicy:丟棄隊列中等待中最久的任務(隊列頭的任務),然後將該任務加入隊列
線程池狀態

ThreadPoolExecutor類中有一個ctl變量,我喜歡叫它線程池標誌碼,是一個32位整數,高3位用於標誌線程池的狀態,剩下29位表示池中的當前線程數

關於這個高三位,有以下幾種狀態標識:

  • -1:RUNNING,運行狀態,表示線程池正常工作
  • 0:SHUTDOWN,關閉狀態,表示線程池工作準備結束,不能接受新任務,但可以處理在任務隊列中排隊的任務
  • 1:STOP,停止狀態,表示線程池停止工作,不能接受新任務,也不能處理排隊中任務,同時會中斷執行中的任務
  • 2:TIDYING,收工狀態,表示線程池即將終結,所有任務都已終止,工作線程數爲0,會馬上運行terminated()鉤子方法結束線程池的生命週期
  • 3:TERMINATED,終結狀態,表示線程池壽命結束,terminated()方法調用結束

對這些狀態之間的轉換方式有興趣的也可以瞭解一下:

  • RUNNING -> SHUTDOWN:調用shutdown方法後
  • (RUNNING or SHUTDOWN) -> STOP:調用shutdownNow方法後
  • SHUTDOWN -> TIDYING:當隊列和池中的工作線程均爲空時
  • STOP -> TIDYING:terminated方法執行完畢後
執行策略

關於一個任務提交線程池之後,將執行什麼樣的行爲,以下步驟應該能說得很清晰了:

  1. 提交一個任務到線程池中
  2. 判斷池中的工作線程數是否小於核心線程數,如果小於,就添加一個線程,然後執行該任務
  3. 判斷隊列是否已滿,如果已滿,就進入第5步,否則將其添加到隊列中
  4. 從隊列中取出該任務,如果移除失敗,就調用拒絕策略來處理
  5. 判斷線程池是否已滿,如果已滿,調用拒絕策略來處理,否則進入下一步
  6. 創建一個新線程到線程池中執行該任務

看完這部分可能你印象不深,甚至根本就不理解,沒關係,我們接下來就要從源碼來分析這些步驟到底是怎麼實現的

源碼講解

execute()

與框架源碼比起來,JDK源碼顯得格外親民,我們的關注點只需放在一個方法上即可,這裏直接進入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);
    }

先別急着看,這裏我先來解釋一下其中出現的一些可能有些不太好懂的方法的含義,等到後面再來抽幾個重要的方法講解一下:

  • workerCountOf:獲取池中的當前線程數
  • isRunning: 判斷是否線程池在正常運行狀態
  • addWorker:添加一個任務到線程池

好了,根據提供的這些方法,我們打上詳細的註釋,再來回頭看一遍這個方法:

    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);
    }

和我們在上面總結的完全一致

addWorker(Runable, Boolean)

如果你僅僅希望瞭解一個執行流程,那本篇文章對你來說已經結束了,否則的話,現在纔到了真正的重頭戲,我們來看源碼:

    private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get();
            // 獲取狀態碼
            int rs = runStateOf(c);

			// 如果線程池不在運行狀態,且以下狀態不成立:
			// 		線程池已經調用shutdown()停止運行,同時要添加的任務爲空但任務隊列不爲空
			// 就不進行任務的添加操作,直接返回false
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                // 返回添加失敗
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                // 如果core爲true,就判斷是否超過核心線程數,否則就判斷是否超過最大線程數
                // 如果超過了限制就直接返回false
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                // 通過死循環,不斷嘗試自增工作線程的數量
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                // 自增成功,重新獲取一下線程標識碼的值
                c = ctl.get();
                // 如果線程狀態發生改變,就返回重試
                if (runStateOf(c) != rs)
                    continue retry;
            }
        }

        boolean workerStarted = false;
        boolean workerAdded = false;
        // Worker實現了Runnable接口,是經過封裝後的任務類
        Worker w = null;
        try {
        	// 將任務封裝爲Worker對象,
        	// 同時會調用線程工廠的newThread方法來生成一個執行該任務的線程
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                // 以下爲同步方法,需要使用Lock加鎖
                mainLock.lock();
                try {
                	// 獲取線程池狀態
                    int rs = runStateOf(ctl.get());

					// 如果運行狀態正常,
					// 或雖然調用shutDown()方法停止線程池,但是待添加任務爲空
                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        // 如果線程已經啓動,就表示不能再重新啓動了,則拋出異常
                        if (t.isAlive())
                            throw new IllegalThreadStateException();
                        // 將該線程添加到workers中
                        // workers包含所有的工作線程,必須工作在同步代碼塊中
                        workers.add(w);
                        int s = workers.size();
                        // largestPoolSize是整個線程池曾達到過的最大容量
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                	// 解鎖操作必須寫在finally語句中,因爲Lock在拋出異常時不會自動解鎖
                    mainLock.unlock();
                }
                // 如果添加任務成功,就啓動線程
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
        	// 不管怎麼樣,只要啓動失敗,就會進行操作的回滾
            if (! workerStarted)
                addWorkerFailed(w);
        }
        // 返回是否啓動成功,而不是是否添加任務成功
        return workerStarted;
    }

我把整個方法分爲四部分:

  1. 預檢查:保證線程池和任務隊列能正常進行操作
  2. 預操作:提前自增工作線程的個數,通過自旋鎖保證同步
  3. 添加任務:將待執行的任務封裝成爲Worker對象,添加到任務集合中
  4. 執行任務以及異常回滾:啓動工作線程,如果啓動失敗會對之前的操作進行回滾

如果有看不懂的地方,配合代碼中的註釋應該也能差不多看懂,如果只是掃一眼我的總結就抱怨看不懂,那還是建議另尋別處,既然我這裏把註釋完整地補上了,還是希望大家能認真看完,這裏的總結僅僅是一個簡單的概括

這裏有一點難以理解的就是預檢查部分的一段代碼,就是下面這段:

            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

我這裏再詳細講一下,首先,這裏判斷了兩個條件,如果第一個條件滿足且第二個條件不滿足時,會直接返回false,也就是直接失敗。我們反過來想,什麼情況下會繼續執行?這麼一來條件就簡化了很多,也就是以下條件成立時,會接着進行判斷,而不會直接返回失敗:

  • 線程池處於正常運行狀態
  • 線程池處於關閉狀態,且待添加的任務爲空,並且任務隊列不爲空(此時會處理排隊任務)

總結

這樣整個線程池的原理就講完了,在總結部分我也不準備再重複一遍流程了,因爲流程已經都在上面的概念部分概括了,我這裏就說幾點要注意的吧:

  • 如果你不手動傳入線程工廠,線程池會提供一個默認的線程工廠,而不是簡單的new Thread(Runnable)創建線程
  • 線程池調用shutdown()方法後,一定記住線程池並不會立馬關閉,而是會接着處理排隊任務
  • …等以後想到了再補充
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章