JAVA線程池源碼系列——1、線程池如何執行任務

概述

之前我寫了一篇博客,系統的介紹了線程池相關知識。感興趣的讀者可以點擊這裏查看之前的博客。

最近我打算實現一個簡單的線程池,在實現過程遇到很多問題,從中發現很多之前沒有搞懂的知識點。爲了查缺補漏,我打算整理一遍線程池核心功能的完整源碼。


線程池如何執行任務

本篇主要介紹線程池執行一個任務的全過程。爲了便於理解,我打算按照源碼執行順序展開,其中每個方法儘可能通過流程圖的方式展示。

在閱讀本篇博客前,需要你對線程池有一個大概的瞭解,本篇不會具體解釋某個屬性的作用,如果對線程池本身還不太熟悉的話,建議先點擊概述中的鏈接初步瞭解一下。


1、execute(Runnable command)

進入正題,線程池無論通過哪種方式執行任務,最終都會調用 execute() 方法。下面我們直接給出 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);
}

關於該方法的邏輯,我們直接通過以下流程圖介紹:

在這裏插入圖片描述
首先就流程圖中,幾個我認爲比較迷惑的點先列出來並給出我的理解:

  • 爲什麼在任務入隊後,還需要判斷線程池狀態是否停止,並嘗試刪除任務呢?

    答:這裏實際上是做了一個 複檢 的操作,爲的是防止在任務入隊期間線程池停止。

  • 爲什麼要分兩種線程?一種以被執行任務爲參數,一種以null爲參數?

    答:這裏的任務參數表示 第一個執行的任務。也就是說,該線程啓動後有一個初始任務。如果任務入隊的話,就不需要創建線程默認執行,因爲它遲早會從隊列中取出並執行。

  • 核心線程和非核心的區別有哪些?

    答:我認爲兩者本身沒有區別,只是在創建線程過程中,核心線程和非核心線程進行不同的邏輯判斷,核心線程根據核心線程數判斷,非核心線程根據所有線程數判斷。

總結:就我個人理解,execute() 方法主要判斷 是否需要創建 Worker對象 。而在線程池中,Worker類也就是線程工作類,也就是說該方法主要判斷當前任務是否需要創建新線程來執行。

根據閱讀上述代碼,我們會發現,只有以下三種情況纔會創建新的線程:

  1. 線程池中工作線程數量小於 核心線程數量 時,創建 核心線程
  2. 線程池中阻塞隊列不爲空,工作線程數量等於0時,創建 非核心線程
  3. 創建核心線程失敗,任務入隊失敗時,創建 非核心線程

而這三種情況對應的實際場景依次分別是:

  1. 核心線程數量不夠,補充新的核心線程執行當前任務
  2. 線程池中沒有線程,並且阻塞隊列中存在未執行的任務,將任務入隊,按照隊列順序依次執行
  3. 核心線程已滿,阻塞隊列已滿,創建非核心線程執行當前任務

當然上述只是線程執行的第一步,下面我們來閱讀具體創建工作線程的代碼:


2、addWorker(Runnable firstTask, boolean core)

線程池執行 execute() 方法後,最終都要通過 addWorker() 方法創建的線程執行。下面我們直接給出 addWork() 方法的源代碼:

private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);
        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
            if (runStateOf(c) != rs)
                continue retry;
        }
    }
    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        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())
                        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;
}

關於該方法的邏輯,我們也通過流程圖的方式展開:

添加工作線程流程圖
首先我們解釋流程圖中的 特殊情況 :線程池處於 ShutDown狀態,當前任務爲空,並且阻塞隊列不爲空時。這也就對應上述 execute() 方法的第二種情況,唯一區別是,execute() 方法執行時線程池還處於 Running 狀態,而這裏線程池已經處於ShutDown狀態。

通過這裏也可以反映出線程池策略:線程池停止後不會處理新方法,但緩存隊列中的方法還會執行

關於上述流程圖,我也列舉出部分疑問我和理解:

  • 上述代碼中,爲什麼要加解鎖?

    答:加解鎖期間,主要是爲了處理部分線程不安全的變量。如:workers 採用 HashSet 數據結構,largestPoolSize 是 int 類型的,這兩個全局變量都是線程不安全的。

  • 上述代碼中,創建線程後,爲什麼要判斷線程是否啓動?

    答:關於這個問題我也不能理解:新創建的線程在沒有啓動時,isAlive() 方法一定會返回 false,也就是說該判斷一定不同通過。我懷疑該方法是爲了確定創建對象期間是否出錯,也就是初始化thread屬性期間沒有出現異常。

總結:addWorker() 方法主要完成 創建工作線程,並啓動該工作線程。有且僅當工作線程創建成功並啓動時返回 True。該方法在 execute() 方法的基礎上,增加了判斷線程數量的操作,其餘都是一些維護線程池屬性的操作。除此之外,它還實現了非Running狀態,拒絕執行任務的邏輯。

execute()方法 和 addWorker() 方法都是在主線程層面調用線程池執行任務,下面我們具體看一下工作線程是如何執行任務,也就是Worker類的源碼。


3、Worker 類

Worker類是線程池中用來封裝工作線程的核心類,我們直接看它的源碼:

private final class Worker
    extends AbstractQueuedSynchronizer
    implements Runnable
{
    private static final long serialVersionUID = 6138294804551838833L;

    final Thread thread;
    Runnable firstTask;
    volatile long completedTasks;

    Worker(Runnable firstTask) {
        setState(-1);
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }
    
    public void run() {
        runWorker(this);
    }
    
	// 略
}

首先該類繼承 AbstractQueuedSynchronizer 類,簡化部分同步操作。其實也很容易猜到,這裏簡化的是每個任務執行所獲取和釋放的鎖定。

其次該類實現了 Runnable 接口,也就是說可以將該類對象作爲參數創建線程,啓動線程來執行該類的run() 方法。

最後該類有兩個重要的屬性:thread 和 firstTask。thread 表示工作線程本身,firstTask 表示該工作線程的首任務。在構造方法中,通過Worker對象本身作爲參數創建線程 thread,而該 thread 又是 Worker 對象的屬性。我們在啓動該Worker對象的thread屬性時,實際上就是執行Worker對象的run()方法,也就是線程體

關於這樣做的好處我是這樣理解的:

  1. 線程池可以從外部維護 Worker對象,而不是 Thread 對象,易於管理
  2. 可以通過Worker對象實現部分方法,通過調用這些方法操作線程,而不是通過thread對象
  3. 可以在Worker對象中維護屬性記錄線程的狀態,不用每次調用Thread方法判斷

下面我們來看 runWorker() 方法的源碼,熟悉線程體都做了什麼


4、runWorker(Worker w)

所有工作線程最終都會走到 runWorker() 方法來執行任務,這裏我直接貼出源碼:

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock();
    boolean completedAbruptly = true;
    try {
        while (task != null || (task = getTask()) != null) {
            w.lock();
            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 {
        processWorkerExit(w, completedAbruptly);
    }
}

上述代碼我們依舊通過流程圖的形式展開,如下所示(流程圖中部分非關鍵邏輯可能存在錯誤,例如線程調用interrupt()方法並不會立即停止):執行任務流程
這個方法的核心功能就是告訴我們,工作線程通過直接執行Runnable對象方法的run方法執行任務

其中 執行前期操作執行後期操作 都可能產生異常,方法體內可以做異常拋出等操作。

需要注意的一點是:有且僅當getTask()方法拋出異常時,纔會導致停止標誌以true的形式執行 processWorkerExit() 方法。關於該方法的邏輯本篇暫時不做結束,後續再其他博客中展開。

通過該方法,我們可以看出線程池中,工作線程能夠複用的主要原理是:通過單線程執行多個任務的 run() 方法。其中在處理完某個任務後,通過 getTask() 方法獲取新的任務執行。

最後,我們來看一下 getTask() 方法的實現原理。


5、getTask()

在線程池中,工作線程循環調用 getTask() 方法獲取任務進行執行,這也是線程池單線程執行多任務的原理。我們直接看源碼:

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);
        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())) {
            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;
        }
    }
}

其中該方法邏輯有點繞,我先通過流程圖的方式給出大體邏輯,下面通過文字對比較繞的模塊單獨介紹:
獲取任務方法
判斷線程池是否停止:可以理解爲線程池是否不工作了,主要表示以下兩種場景:

  1. 線程池狀態爲STOP及以後
  2. 線程池狀態爲SHUTDOWN,並且阻塞隊列已經爲空

這裏主要爲了區分線程池處於SHUTDOWN,但是隊列不爲空的情況。我們知道,線程池處於SHUTDOWN狀態時,還是會處理阻塞隊列中的任務的,也就是還在工作的。

判斷是否超時回收:當存在以下兩種情況時,超時線程纔會被回收:

  1. 配置 allowCoreThreadTimeOut 爲 True,也就是所有線程空閒都會回收
  2. 線程數超過最大核心線程數,也就可以理解爲非核心線程空閒都會被回收

判斷線程是否可回收:判斷一個線程是否可回收主要集中在以下兩種情況,其中無論哪種情況至少得滿足下述兩種情況中的一種:

  1. 線程數大於1,也就是說有線程可回收
  2. 阻塞隊列爲空,也就是說當前沒有任務需要執行

判斷可回收的情況如下:

  1. 線程數大於最大線程數
  2. 線程已超時,並且該線程超時會被回收

流程圖中比較模糊的幾個判斷及解釋已經給出,現在我們再來看 getTask()方法。

我理解 getTask() 方法的核心作用是 控制工作線程的週期。可以總結爲:

  • 如果當前線程是可回收的,超過最長空閒時返回null,線程執行 processWorkerExit() 方法被回收
  • 如果線程是不可回收的,就阻塞線程,直到有任務時返回任務,交給工作線程處理

最後我們再來聊聊線程回收,通過總結上述代碼,我認爲只有三種情況纔會回收線程:

  1. 線程池不工作了
  2. 超時可回收線程超時了
  3. 線程數超過最大線程,並且當前沒有任務

**那麼線程池是如何做到阻塞工作線程不讓它被回收的呢?又是如何做到回收線程?

:阻塞隊列的 take() 方法在隊列爲空時會阻塞,因此線程也會阻塞,此時線程就會一直等待任務,不被回收。阻塞隊列的 poll() 方法不會阻塞,如果線程池爲空,直接返回null。回到 runWorker() 方法,如果任務爲空,就會跳出循環執行 processWorkerExit() 方法,該方法會通過 tryTerminate() 方法停止線程並回收。

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