從源碼來看JDK8線程池ThreadPoolExecutor的實現原理(二)

上一篇文章,咱們剛講完了ThreadPoolExecutor線程池的execute的大體總體實現,以及內部的重要屬性,尚未看過的,要想看這篇文章,必須先連接上一篇才能繼續: juejin.im/post/5d67e5…數據庫

這節咱們將接着看線程池是如何終止和中止運行的.直接上源碼:編程

tryTerminate方法安全

tryTerminate方法根據線程池狀態進行判斷是否結束線程池, 什麼時候調用: 1.若是加入工做隊列失敗,嘗試調用。 2.processWorkerExit執行work隊列中的worker退出。 3.shutdown 啓動有序的關閉線程池中的線程,再也不接收新任務,而後關閉工做線程。 代碼以下:bash

final void tryTerminate() {
    for (;;) {
        int c = ctl.get();
        /*
         * 當前線程池的狀態爲如下幾種狀況時,直接返回:
         * 1. RUNNING,由於還在運行中,不能中止;
         * 2. TIDYING或TERMINATED,由於線程池中已經沒有正在運行的線程了;
         * 3. SHUTDOWN而且等待隊列非空,這時要執行完workQueue中的task;
         */
        if (isRunning(c) ||
            runStateAtLeast(c, TIDYING) ||
            (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
            return;
        // 若是線程數量不爲0,則中斷一個空閒的工做線程,並返回
        if (workerCountOf(c) != 0) { // Eligible to terminate
            interruptIdleWorkers(ONLY_ONE);  //該類的解析在上一篇文章,在頂部
            return;
        }
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            // 這裏嘗試設置狀態爲TIDYING,若是設置成功,則調用terminated方法
            if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                try {
                    // terminated方法默認什麼都不作,留給子類實現
                    terminated();
                } finally {
                    // 設置狀態爲TERMINATED
                    ctl.set(ctlOf(TERMINATED, 0));
                    termination.signalAll();
                }
                return;
            }
        } finally {
            mainLock.unlock();
        }
        // else retry on failed CAS
    }
}
複製代碼

interruptIdleWorkers(ONLY_ONE);的做用是由於在getTask方法中執行workQueue.take()時,若是不執行中斷會一直阻塞。併發

在下面介紹的shutdown方法中,會中斷全部空閒的工做線程,若是在執行shutdown時工做線程沒有空閒,而後又去調用了getTask方法,這時若是workQueue中沒有任務了,調用workQueue.take()時就會一直阻塞。因此每次在工做線程結束時調用tryTerminate方法來嘗試中斷一個空閒工做線程,避免在隊列爲空時取任務一直阻塞的狀況。ide

shutdown方法post

shutdown方法要將線程池切換到SHUTDOWN狀態,並調用interruptIdleWorkers方法請求中斷全部空閒的worker,最後調用tryTerminate嘗試結束線程池。ui

public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        // 安全策略判斷
        checkShutdownAccess();
        // 切換狀態爲SHUTDOWN
        advanceRunState(SHUTDOWN);
        // 中斷空閒線程
        interruptIdleWorkers();
        onShutdown();//在調用shutdown時運行狀態轉換後執行任何進一步的清理。
        這裏是一個空實現,但由子類 ScheduledThreadPoolExecutor能夠用於取消延遲的任務。
    } finally {
        mainLock.unlock();
    }
    // 嘗試結束線程池
    tryTerminate();
}
複製代碼

最終要思考一個問題: 在runWorker方法中,執行任務時對Worker對象w進行了lock操做,爲何要在執行任務的時候對每一個工做線程都加鎖呢?this

在getTask方法中,若是這時線程池的狀態是SHUTDOWN而且workQueue爲空,那麼就應該返回null來結束這個工做線程,而使線程池進入SHUTDOWN狀態須要調用shutdown方法;idea

shutdown方法會調用interruptIdleWorkers來中斷空閒的線程,interruptIdleWorkers持有mainLock,會遍歷workers來逐個判斷工做線程是否空閒。但getTask方法中沒有mainLock;

在getTask中,若是判斷當前線程池狀態是RUNNING,而且阻塞隊列爲空,那麼會調用workQueue.take()進行阻塞;

若是在判斷當前線程池狀態是RUNNING後,這時調用了shutdown方法把狀態改成了SHUTDOWN,這時若是不進行中斷,那麼當前的工做線程在調用了workQueue.take()後會一直阻塞而不會被銷燬,由於在SHUTDOWN狀態下不容許再有新的任務添加到workQueue中,這樣一來線程池永遠都關閉不了了;

由上可知,shutdown方法與getTask方法(從隊列中獲取任務時)存在競態條件; 解決這一問題就須要用到線程的中斷,也就是爲何要用interruptIdleWorkers方法。在調用workQueue.take()時,若是發現當前線程在執行以前或者執行期間是中斷狀態,則會拋出InterruptedException,解除阻塞的狀態;

可是要中斷工做線程,還要判斷工做線程是不是空閒的,若是工做線程正在處理任務,就不該該發生中斷;

因此Worker繼承自AQS,在工做線程處理任務時會進行lock,interruptIdleWorkers在進行中斷時會使用tryLock來判斷該工做線程是否正在處理任務,若是tryLock返回true,說明該工做線程當前未執行任務,這時才能夠被中斷。

下面就來分析一下interruptIdleWorkers方法。

private void interruptIdleWorkers() {
    interruptIdleWorkers(false);
}
private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers) {
            Thread t = w.thread;
            if (!t.isInterrupted() && w.tryLock()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                } finally {
                    w.unlock();
                }
            }
            if (onlyOne)
                break;
        }
    } finally {
        mainLock.unlock();
    }
}
複製代碼

interruptIdleWorkers遍歷workers中全部的工做線程,若線程沒有被中斷tryLock成功,就中斷該線程。

shutdownNow

public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(STOP);
        // 中斷全部工做線程,不管是否空閒
        interruptWorkers();
        // 取出隊列中沒有被執行的任務
        tasks = drainQueue();
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
    return tasks;
}
複製代碼

當即中止全部的執行任務,並將隊列中的任務返回

shutdown和shutdownNow區別 shutdown和shutdownNow這兩個方法的做用都是關閉線程池,流程大體相同,只有幾個步驟不一樣,以下 加鎖 檢查關閉權限 CAS改變線程池狀態 設置中斷標誌(線程池不在接收任務,隊列任務會完成)/中斷當前執行的線程 調用onShutdown方法(給子類提供的方法)/獲取隊列中的任務 解鎖 嘗試將線程池狀態變成終止狀態TERMINATED 結束/返回隊列中的任務

如何使用線程池

如何合理配置線程池參數? 要想合理的配置線程池,就必須首先分析任務特性,能夠從如下幾個角度來進行分析:

任務的性質:CPU密集型任務,IO密集型任務和混合型任務。 任務的優先級:高,中和低。 任務的執行時間:長,中和短。 任務的依賴性:是否依賴其餘系統資源,如數據庫鏈接。

任務性質不一樣的任務能夠用不一樣規模的線程池分開處理。

CPU密集型任務配置儘量少的線程數量,如配置Ncpu+1個線程的線程池。

IO密集型任務則因爲須要等待IO操做,線程並非一直在執行任務,則配置儘量多的線程,如2xNcpu。

混合型的任務,若是能夠拆分,則將其拆分紅一個CPU密集型任務和一個IO密集型任務,只要這兩個任務執行的時間相差不是太大,那麼分解後執行的吞吐率要高於串行執行的吞吐率,若是這兩個任務執行時間相差太大,則不必進行分解。咱們能夠經過Runtime.getRuntime().availableProcessors()方法得到當前設備的CPU個數。

優先級不一樣的任務可使用優先級隊列PriorityBlockingQueue來處理。它可讓優先級高的任務先獲得執行,須要注意的是若是一直有優先級高的任務提交到隊列裏,那麼優先級低的任務可能永遠不能執行。

執行時間不一樣的任務能夠交給不一樣規模的線程池來處理,或者也可使用優先級隊列,讓執行時間短的任務先執行。

依賴數據庫鏈接池的任務,由於線程提交SQL後須要等待數據庫返回結果,若是等待的時間越長CPU空閒時間就越長,那麼線程數應該設置越大,這樣才能更好的利用CPU。

而且,阻塞隊列最好是使用有界隊列,若是採用無界隊列的話,一旦任務積壓在阻塞隊列中的話就會佔用過多的內存資源,甚至會使得系統崩潰。

線程池的監控

經過線程池提供的參數進行監控。線程池裏有一些屬性在監控線程池的時候可使用

getTaskCount:線程池已經執行的和未執行的任務總數; getCompletedTaskCount:線程池已完成的任務數量,該值小於等於taskCount; getLargestPoolSize:線程池曾經建立過的最大線程數量。經過這個數據能夠知道線程池是否滿過,也就是達到了maximumPoolSize; getPoolSize:線程池當前的線程數量; getActiveCount:當前線程池中正在執行任務的線程數量。 經過這些方法,能夠對線程池進行監控,在ThreadPoolExecutor類中提供了幾個空方法,如beforeExecute方法,afterExecute方法和terminated方法,能夠擴展這些方法在執行前或執行後增長一些新的操做,例如統計線程池的執行任務的時間等,能夠繼承自ThreadPoolExecutor來進行擴展。

總結 本文比較詳細的分析了線程池的工做流程,整體來講有以下幾個內容:

分析了線程的建立,任務的提交,狀態的轉換以及線程池的關閉; 這裏經過execute方法來展開線程池的工做流程,execute方法經過corePoolSize,maximumPoolSize以及阻塞隊列的大小來判斷決定傳入的任務應該被當即執行,仍是應該添加到阻塞隊列中,仍是應該拒絕任務。 介紹了線程池關閉時的過程,也分析了shutdown方法與getTask方法存在競態條件; 在獲取任務時,要經過線程池的狀態來判斷應該結束工做線程仍是阻塞線程等待新的任務,也解釋了爲何關閉線程池時要中斷工做線程以及爲何每個worker都須要lock。 在向線程池提交任務時,除了execute方法,還有一個submit方法,submit方法會返回一個Future對象用於獲取返回值。

摘自: 《Java併發編程的藝術》 JDK8源碼 深刻理解Java線程池 www.ideabuffer.cn/2017/04/04/

相關文章
相關標籤/搜索