細說JUC的執行緒池架構

qzlzzz發表於2021-10-08

前言

執行緒的建立是需要JVM和OS(作業系統)相互配合的,一次的建立要花費許多的資源。

1.首先,JVM要為該執行緒分配堆疊和初始化大量記憶體塊,棧記憶體至少是1MB。
2.其次便是要進行系統的呼叫,在OS中建立和註冊本地的執行緒。

在Java的高併發場景下頻繁的建立和銷燬執行緒,一方面是記憶體塊的頻繁分配和回收,另一方面是作業系統頻繁註冊執行緒和銷燬,記憶體資源利用率不高的同時,也增加了時間的成本,這是非常低效的。我們要做的是線上程執行完使用者程式碼邏輯塊後,儲存該執行緒,等待下一次使用者程式碼邏輯塊來到時,繼續去運用該執行緒去完成這個任務,這樣不僅減少了執行緒頻繁的建立和銷燬,同時提高了效能。具體的實現便是執行緒池技術。

JUC執行緒池架構

執行緒池技術主要來自於java.util.concurrent包(俗稱JUC),該包是JDK1.5以後引進來的,主要是完成高併發,多執行緒的一個工具包。

執行緒池主要解決了執行緒的排程,維護,建立等問題,它在提高了執行緒的利用率的同時還提高了效能。

在JUC中,有關執行緒池的類和介面大致如圖下所示:

接下來讓我們一個一個解析每個介面和類吧。

Exector介面

我們從原始碼來看Exector

public interface Executor {
    void execute(Runnable command);
}

我們可以看到Exector只有一個介面方法便是execute()方法,該方法是定義是執行緒在未來的某個時間執行給定的目標執行任務,也就是說執行緒池中的執行緒執行目標任務的方法。

ExecutorService介面

該介面繼承了Executor因此它也繼承了execute()方法,同時它本身也擴充套件了一些重要的介面方法,我們通過原始碼看一下幾個比較常用的方法

public interface ExecutorService extends Executor {

    void shutdown();

    List<Runnable> shutdownNow();

    boolean isShutdown();

    boolean isTerminated();

    <T> Future<T> submit(Callable<T> task);

    <T> Future<T> submit(Runnable task, T result);

    Future<?> submit(Runnable task);
}
  • shutdown()介面方法的定義是關閉執行緒池,它與shutdownNow()方法不同的點在於,它不會中止正在執行的執行緒,它也會把未完成的目標任務完成了,此時執行緒池的狀態未SHUTDOWN,執行回撥函式後關閉執行緒池。
  • shutdownNow()介面方法的定義也是關閉執行緒池,它與shutdown()方法不同的點在於,它會中止正在執行的執行緒,清空已提交但未執行的目標任務,返回已完成的目標任務,同時執行緒池的狀態為STOP,執行回撥函式後關閉執行緒池。
  • isShutdown()介面方法的定義是判斷當前的執行緒池狀態是否是SHUTDOWN狀態,是返回true,不是返回false。
  • isTerminated()介面方法的定義是判斷當前執行緒池狀態是否是TERMINATED狀態,也就是判斷當前執行緒池是否已經關閉,不是返回flase,是返回true。
  • submit()介面方法的定義與execute()方法類似,也是提交目標任務給執行緒池,執行緒池中的執行緒在適合的時機去執行該目標任務,它與execute()方法不同的點在兩個:一方面是submit()方法的形參可以有是Callable型別的,也可以是Runnable型別的,而execute()方法僅能接收Runnable型別的,另一方面是submit()方法的返回值型別是Future,這意味著,我們可以獲取到目標任務的執行結果,以及任務的是否執行、是否取消等情況,而execute()方法的返回值是void型別,這也表示我們獲取不到目標任務的執行情況等資訊。

AbstractExecutorService抽象類

正如第一張圖顯示的:AbstractExecutorService抽象類繼承了ExecutorService介面,這意味著AbstractExecutorService抽象類擁有著父類ExecutorService介面的所有介面方法,同時因為ExecutorService介面又繼承了Executor介面,因此也擁有Executor介面的介面方法。

不僅如此AbstractExecutorService抽象類實現了除shutdown()、shutdownNow()、execute()、isShutdown()、isTerminated()以外的方法,這裡我們主要檢視一下submit()方法的實現,同時也可以加深我們對execute()和submit()的關係與區別。

    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }
    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }

我們來解讀一下原始碼:

  1. 首先判斷傳進來的Runnable型別的物件是否為空,如果為空的話便丟擲一個空指標異常。
  2. 若不為空,則將目前的Runnable型別物件傳入newTaskFor方法,我們走進newTaskFor方法可以發現,其將Runnable型別物件修飾成了一個FutureTask型別的物件,FutureTask是實現了RunnableFuture介面的實現類,因此可以將其賦予給ftask。
  3. 隨後,呼叫了execute方法,將ftask作為目標任務,傳入執行緒池,等待執行緒池排程執行緒執行這個任務,最後再返回ftask,便於呼叫執行緒監控目標任務的執行情況和執行結果。

從原始碼分析我們可以得知,submit()方法本質上還是呼叫了executor()方法,只不過將Runnable型別的物件修飾成了FutureTask型別,讓其擁有監控執行任務的能力而已。

有關Callable介面和FutureTask實現類以及RunnableFuture介面的詳細資訊可以查閱筆者另一篇隨筆: https://www.cnblogs.com/qzlzzz/p

ThreadPoolExecutor執行緒池實現類

ThreadPoolExecutor繼承了AbstractExecutorService抽象類,因此也就擁有了AbstractExecutorService抽象類繼承的實現了的介面方法和AbstractExecutorService抽象類所繼承的未實現的介面方法。

在此前提下,ThreadPoolExecutor不僅實現了AbstractExecutorService抽象類未實現的介面方法,同時其內部真正的實現了一個執行緒池,且實現了執行緒池的排程,管理,維護,空閒執行緒的存活時間,預設的執行緒工廠,和阻塞佇列,核心執行緒數,最大執行緒數,淘汰策略等功能。我們也可得知ThreadPoolExecutor是執行緒池技術的核心、重要類。"由於本隨筆僅說JUC的執行緒池架構,因此不多描述執行緒池的實現等其核心功能"

這裡我們著眼於ThreadPoolExecutor對execute()方法的實現,首先我們來看其原始碼:

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        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. 首先會判斷傳入進來的Runnable型別的物件是否為空,如果為空則丟擲一個空指標異常。

2. 隨後獲取ctl的值,ctl是原子類,在定義時它的初始值是 -536870912,獲取到值後賦給c變數,c變數傳入workerCountOf()方法,在方法的內部進行了或運算,以此來獲取執行緒池的執行緒數,如果執行緒池的執行緒數比定義執行緒池時所設定的核心執行緒數要少的話,不管執行緒池裡的執行緒是否空閒,都會新建一個執行緒。

3. 判斷為true的話,進入到巢狀if()中的addWorker()方法。

這裡我們再來探尋一下addWorker()方法的原始碼:

    private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (int c = ctl.get();;) {
            // Check if queue empty only if necessary.
            if (runStateAtLeast(c, SHUTDOWN)
                && (runStateAtLeast(c, STOP)
                    || firstTask != null
                    || workQueue.isEmpty()))
                return false;

            for (;;) {
                if (workerCountOf(c)
                    >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
                    return false;
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                if (runStateAtLeast(c, SHUTDOWN))
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }

        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 {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    int c = ctl.get();

                    if (isRunning(c) ||
                        (runStateLessThan(c, STOP) && firstTask == null)) {
                        if (t.getState() != Thread.State.NEW)
                            throw new IllegalThreadStateException();
                        workers.add(w);
                        workerAdded = true;
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

在addWorker()方法裡面我們可以看到充斥著大量執行緒池在SHUTDOWN和STOP狀態時,執行緒池該怎樣去運作,當執行緒池中的執行緒數達到核心執行緒數,執行緒池又如何去做,以及如何選取空餘的執行緒去執行目標任務或者在阻塞佇列中的目標任務等排程,建立功能。

在如此長的一段程式碼中我們關注這幾行:

            //第一段程式碼
            w = new Worker(firstTask);
            final Thread t = w.thread;

以及

                //第二段程式碼
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }

首先是第一段程式碼,我們可以看到它將目標任務Runnable型別的物件修飾成了Worker型別,我們翻看一下Worker類:

    private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable{
        //省略其他程式碼
  
        Runnable firstTask;
        
        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }
        
        public void run() {
            runWorker(this);
        }

        //省略其他程式碼
}

從Worker的構造方法中我們可以知道其將目標任務,傳給了自己的例項屬性,同時由於自己本身是Runnable的實現類,因此可以以自己本身作為引數傳入到執行緒工廠的構造執行緒方法中,而自己本身實現的run()方法中又呼叫了runWorker()方法,runWorker()方法的引數又是當前Worker的例項本身,如果讀者有意深入的話,會發現runWorker()方法體中有一段是task.run()去執行目標任務,其餘的程式碼則是回撥函式的呼叫。

也就是說執行緒工廠建立的執行緒,如果啟動該執行緒去執行的話,是執行Worker類中的run()方法,也就會去執行run()方法中的runWorker()方法。

然後我們繼續來看第一段程式碼,其使用控制程式碼w獲取到thread,賦予給了Thread型別的t變數。第一段程式碼結束,再到第二段程式碼中使用了t.start()來啟動這個執行緒去執行目標任務,再將這個任務的工作狀態設為ture。

至此,兩段程式碼探討結束。

4. 最後回到execute()方法中,繼續走下去便是一些執行緒池拒絕策略的判斷,在這裡就不過多敘述了。

ScheduledExecutorSerive介面

從關係圖可以得知ScheduledExecutorService介面繼承了ExecutorService介面,這說明ScheduledExecutorService介面擁有著ExecutorService介面的介面方法,同時除了ExecutorService的提交、執行、判斷執行緒池狀態等的介面方法之外,ScheduledExecutorService介面還擴充了一些介面方法。

這裡我們從介面定義中來解讀ScheduledExecutorService究竟增加了那些功能。

public interface ScheduledExecutorService extends ExecutorService {

    public ScheduledFuture<?> schedule(Runnable command,
                                       long delay, TimeUnit unit);

    public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                           long delay, TimeUnit unit);

    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit);

    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);
}
  • 可以看出schedule()有兩種過載方法,區別在於第一種方法接收的形參是Runnable型別的,第二種方法接收的形參是Callable型別的。其作用都是前一次執行結束到下一次執行開始的時間是delay,單位是unit。
  • scheduleAtFixedRate()介面方法的定義是首次執行目標任務的時間延遲initialDelay,兩個目標任務開始執行最小間隔時間是delay,其單位都是unit。
  • scheduleWithFixedDelay()介面方法的定義與schedule()方法類似,只不過是首次執行的時間延遲initialDelay,單位是unit。

注意上面的方法都是週期的,也就是說會週期地執行目標任務。

ScheduledThreadPoolExecutor執行緒池實現類

ScheduledThreadPoolExecutor繼承了ThreadPoolExecutor,同時實現了ScheduledExecutorSerive介面,這意味著ScheduledThreadPoolExecuotr不僅擁有了ThreadPoolExecutor實現的執行緒池,同ScheduledExecutorService介面繼承的介面方法也無需其實現,因為ThreadPoolExecutor和AbstractExecutorService已經幫其實現了。在此基礎上,ScheduledThreadPoolExecutor實現了ScheduledExecutorService介面擴充的方法,這使得ScheduledThreadPoolExecutor成為一個執行"延時"和"週期性"任務的可排程執行緒池。

至此,JUC執行緒池架構也逐漸清晰了起來,Exector介面定義了最重要的execute方法,ExecutorService 則擴充了提交和執行的方法,也擴充套件了監控執行緒池的方法、AbstractExecutorService抽象類則負責實現了ExecutorService 介面擴充的方法,因為ThreadPoolExecutor類內部實現了執行緒池,所以監控執行緒池的方法和execute方法等重要的方法自然也交給了其實現。最後的ScheduledThreadPoolExecuto類其實也是在整個完整的執行緒池技術上,擴充了執行緒池的一些功能。

結尾

一定要吃早餐,學習的過程需注意自己的身體才行。

相關文章