手寫線程池,對照學習ThreadPoolExecutor線程池實現原理!


做者:小傅哥
博客:https://bugstack.cn
Github:https://github.com/fuzhengwei/CodeGuide/wikihtml

沉澱、分享、成長,讓本身和他人都能有所收穫!😄java

1、前言

人看手機,機器學習!git

正好是2020年,看到這張圖仍是蠻有意思的。之前小時候總會看到一些科技電影,講到機器人會怎樣怎樣,但沒想到人彷佛被娛樂化的東西,搞成了低頭族、大肚子!程序員

當意識到這一點時,其實很是懷念小時候。放假的早上跑出去,喊上三五個夥伴,要不下河摸摸魚、彈彈玻璃球、打打pia、跳跳房子!一天下來真的不會感受累,但如今若是是放假的一天,你的娛樂安排,不少時候會讓頭很累!github

就像,你有試過學習一天英語頭疼,仍是刷一天抖音頭疼嗎?或者玩一天遊戲與打一天球!若是你意識到了,那麼爭取放下一會手機,適當娛樂,鍛鍊保持個好身體!面試

2、面試題

謝飛機,小記!,上次吃虧在線程上,這可能一次坑掉兩次嗎!多線程

謝飛機:你問吧,我準備好了!!!架構

面試官:嗯,線程池狀態是如何設計存儲的?併發

謝飛機:這!下一個,下一個!機器學習

面試官:Worker 的實現類,爲何不使用 ReentrantLock 來實現呢,而是本身繼承AQS?

謝飛機:我...!

面試官:那你簡述下,execute 的執行過程吧!

謝飛機:再見!

3、線程池講解

1. 先看個例子

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(10));
threadPoolExecutor.execute(() -> {
    System.out.println("Hi 線程池!");
});
threadPoolExecutor.shutdown();

// Executors.newFixedThreadPool(10);
// Executors.newCachedThreadPool();
// Executors.newScheduledThreadPool(10);
// Executors.newSingleThreadExecutor();

這是一段用於建立線程池的例子,相信你已經用了不少次了。

線程池的核心目的就是資源的利用,避免重複建立線程帶來的資源消耗。所以引入一個池化技術的思想,避免重複建立、銷燬帶來的性能開銷。

那麼,接下來咱們就經過實踐的方式分析下這個池子的構造,看看它是如何處理線程的。

2. 手寫一個線程池

2.1 實現流程

爲了更好的理解和分析關於線程池的源碼,咱們先來按照線程池的思想,手寫一個很是簡單的線程池。

其實不少時候一段功能代碼的核心主邏輯可能並無多複雜,但爲了讓核心流程順利運行,就須要額外添加不少分支的輔助流程。就像我常說的,爲了保護手才把擦屁屁紙弄那麼大!

圖 21-1 線程池簡化流程

關於圖 21-1,這個手寫線程池的實現也很是簡單,只會體現出核心流程,包括:

  1. 有n個一直在運行的線程,至關於咱們建立線程池時容許的線程池大小。
  2. 把線程提交給線程池運行。
  3. 若是運行線程池已滿,則把線程放入隊列中。
  4. 最後當有空閒時,則獲取隊列中線程進行運行。

2.2 實現代碼

public class ThreadPoolTrader implements Executor {

    private final AtomicInteger ctl = new AtomicInteger(0);

    private volatile int corePoolSize;
    private volatile int maximumPoolSize;

    private final BlockingQueue<Runnable> workQueue;

    public ThreadPoolTrader(int corePoolSize, int maximumPoolSize, BlockingQueue<Runnable> workQueue) {
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
    }

    @Override
    public void execute(Runnable command) {
        int c = ctl.get();
        if (c < corePoolSize) {
            if (!addWorker(command)) {
                reject();
            }
            return;
        }
        if (!workQueue.offer(command)) {
            if (!addWorker(command)) {
                reject();
            }
        }
    }

    private boolean addWorker(Runnable firstTask) {
        if (ctl.get() >= maximumPoolSize) return false;

        Worker worker = new Worker(firstTask);
        worker.thread.start();
        ctl.incrementAndGet();
        return true;
    }

    private final class Worker implements Runnable {

        final Thread thread;
        Runnable firstTask;

        public Worker(Runnable firstTask) {
            this.thread = new Thread(this);
            this.firstTask = firstTask;
        }

        @Override
        public void run() {
            Runnable task = firstTask;
            try {
                while (task != null || (task = getTask()) != null) {
                    task.run();
                    if (ctl.get() > maximumPoolSize) {
                        break;
                    }
                    task = null;
                }
            } finally {
                ctl.decrementAndGet();
            }
        }

        private Runnable getTask() {
            for (; ; ) {
                try {
                    System.out.println("workQueue.size:" + workQueue.size());
                    return workQueue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private void reject() {
        throw new RuntimeException("Error!ctl.count:" + ctl.get() + " workQueue.size:" + workQueue.size());
    }

    public static void main(String[] args) {
        ThreadPoolTrader threadPoolTrader = new ThreadPoolTrader(2, 2, new ArrayBlockingQueue<Runnable>(10));

        for (int i = 0; i < 10; i++) {
            int finalI = i;
            threadPoolTrader.execute(() -> {
                try {
                    Thread.sleep(1500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("任務編號:" + finalI);
            });
        }
    }

}

// 測試結果

任務編號:1
任務編號:0
workQueue.size:8
workQueue.size:8
任務編號:3
workQueue.size:6
任務編號:2
workQueue.size:5
任務編號:5
workQueue.size:4
任務編號:4
workQueue.size:3
任務編號:7
workQueue.size:2
任務編號:6
workQueue.size:1
任務編號:8
任務編號:9
workQueue.size:0
workQueue.size:0

以上,關於線程池的實現仍是很是簡單的,從測試結果上已經能夠把最核心的池化思想體現出來了。主要功能邏輯包括:

  • ctl,用於記錄線程池中線程數量。
  • corePoolSizemaximumPoolSize,用於限制線程池容量。
  • workQueue,線程池隊列,也就是那些還不能被及時運行的線程,會被裝入到這個隊列中。
  • execute,用於提交線程,這個是通用的接口方法。在這個方法裏主要實現的就是,當前提交的線程是加入到worker、隊列仍是放棄。
  • addWorker,主要是類 Worker 的具體操做,建立並執行線程。這裏還包括了 getTask() 方法,也就是從隊列中不斷的獲取未被執行的線程。

,那麼以上呢,就是這個簡單線程池實現的具體體現。但若是深思熟慮就會發現這裏須要不少完善,好比:線程池狀態呢,不可能一直奔跑呀!?線程池的鎖呢,不會有併發問題嗎?線程池拒絕後的策略呢?,這些問題都沒有在主流程解決,也正由於沒有這些流程,因此上面的代碼才更容易理解。

接下來,咱們就開始分析線程池的源碼,與咱們實現的簡單線程池參考對比,會更加容易理解😄!

3. 線程池源碼分析

3.1 線程池類關係圖

圖 21-2 線程池類關係圖

以圍繞核心類 ThreadPoolExecutor 的實現展開的類之間實現和繼承關係,如圖 21-2 線程池類關係圖。

  • 接口 ExecutorExecutorService,定義線程池的基本方法。尤爲是 execute(Runnable command) 提交線程池方法。
  • 抽象類 AbstractExecutorService,實現了基本通用的接口方法。
  • ThreadPoolExecutor,是整個線程池最核心的工具類方法,全部的其餘類和接口,爲圍繞這個類來提供各自的功能。
  • Worker,是任務類,也就是最終執行的線程的方法。
  • RejectedExecutionHandler,是拒絕策略接口,有四個實現類;AbortPolicy(拋異常方式拒絕)DiscardPolicy(直接丟棄)DiscardOldestPolicy(丟棄存活時間最長的任務)CallerRunsPolicy(誰提交誰執行)
  • Executors,是用於建立咱們經常使用的不一樣策略的線程池,newFixedThreadPoolnewCachedThreadPoolnewScheduledThreadPoolnewSingleThreadExecutor

3.2 高3位與低29位

圖 22-3 線程狀態,高3位與低29位

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

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;
private static final int TERMINATED =  3 << COUNT_BITS;

ThreadPoolExecutor 線程池實現類中,使用 AtomicInteger 類型的 ctl 記錄線程池狀態和線程池數量。在一個類型上記錄多個值,它採用的分割數據區域,高3位記錄狀態,低29位存儲線程數量,默認 RUNNING 狀態,線程數爲0個。

3.2 線程池狀態

圖 22-4 線程池狀態流轉

圖 22-4 是線程池中的狀態流轉關係,包括以下狀態:

  • RUNNING:運行狀態,接受新的任務而且處理隊列中的任務。
  • SHUTDOWN:關閉狀態(調用了shutdown方法)。不接受新任務,,可是要處理隊列中的任務。
  • STOP:中止狀態(調用了shutdownNow方法)。不接受新任務,也不處理隊列中的任務,而且要中斷正在處理的任務。
  • TIDYING:全部的任務都已終止了,workerCount爲0,線程池進入該狀態後會調 terminated() 方法進入TERMINATED 狀態。
  • TERMINATED:終止狀態,terminated() 方法調用結束後的狀態。

3.3 提交線程(execute)

圖 22-5 提交線程流程圖

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

在閱讀這部分源碼的時候,能夠參考咱們本身實現的線程池。其實最終的目的都是同樣的,就是這段被提交的線程,啓動執行加入隊列決策策略,這三種方式。

  • ctl.get(),取的是記錄線程狀態和線程個數的值,最終須要使用方法 workerCountOf(),來獲取當前線程數量。`workerCountOf 執行的是 c & CAPACITY 運算
  • 根據當前線程池中線程數量,與核心線程數 corePoolSize 作對比,小於則進行添加線程到任務執行隊列。
  • 若是說此時線程數已滿,那麼則須要判斷線程池是否爲運行狀態 isRunning(c)。若是是運行狀態則把不能被執行的線程放入線程隊列中。
  • 放入線程隊列之後,還須要從新判斷線程是否運行以及移除操做,若是非運行且移除,則進行拒絕策略。不然判斷線程數量爲0後添加新線程。
  • 最後就是再次嘗試添加任務執行,此時方法 addWorker 的第二個入參是 false,最終會影響添加執行任務數量判斷。若是添加失敗則進行拒絕策略。

3.5 添加執行任務(addWorker)

圖 22-6 添加執行任務邏輯流程

private boolean addWorker(Runnable firstTask, boolean core)

第一部分、增長線程數量

retry:
for (;;) {
    int c = ctl.get();
    int rs = runStateOf(c);
    // Check if queue empty only if necessary.
    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;
        // 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 {
            int rs = runStateOf(ctl.get());
            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 {
    if (! workerStarted)
        addWorkerFailed(w);
}
return workerStarted;

添加執行任務的流程能夠分爲兩塊看,上面代碼部分是用於記錄線程數量、下面代碼部分是在獨佔鎖裏建立執行線程並啓動。這部分代碼在不看鎖、CAS等操做,那麼就和咱們最開始手寫的線程池基本同樣了

  • if (rs >= SHUTDOWN &&! (rs == SHUTDOWN &&firstTask == null &&! workQueue.isEmpty())),判斷當前線程池狀態,是否爲 SHUTDOWNSTOPTIDYINGTERMINATED中的一個。而且當前狀態爲 SHUTDOWN、且傳入的任務爲 null,同時隊列不爲空。那麼就返回 false。
  • compareAndIncrementWorkerCount,CAS 操做,增長線程數量,成功就會跳出標記的循環體。
  • runStateOf(c) != rs,最後是線程池狀態判斷,決定是否循環。
  • 在線程池數量記錄成功後,則須要進入加鎖環節,建立執行線程,並記錄狀態。在最後若是判斷沒有啓動成功,則須要執行 addWorkerFailed 方法,剔除到線程方法等操做。

3.6 執行線程(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();
                } finally {
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

其實,有了手寫線程池的基礎,到這也就基本瞭解了,線程池在幹嗎。到這最核心的點就是 task.run() 讓線程跑起來。額外再附帶一些其餘流程以下;

  • beforeExecuteafterExecute,線程執行的先後作一些統計信息。
  • 另外這裏的鎖操做是 Worker 繼承 AQS 本身實現的不可重入的獨佔鎖。
  • processWorkerExit,若是你感興趣,相似這樣的方法也能夠深刻了解下。在線程退出時候workers作到一些移除處理以及完成任務數等,也很是有意思

3.7 隊列獲取任務(getTask)

若是你已經開始閱讀源碼,能夠在 runWorker 方法中,看到這樣一句循環代碼 while (task != null || (task = getTask()) != null)。這與咱們手寫線程池中操做的方式是同樣的,核心目的就是從隊列中獲取線程方法。

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);
        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }
        int wc = workerCountOf(c);
        // Are workers subject to culling?
        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;
        }
    }
}
  • getTask 方法從阻塞隊列中獲取等待被執行的任務,也就是一條條往出拿線程方法。
  • if (rs >= SHUTDOWN ...,判斷線程是否關閉。
  • wc = workerCountOf(c),wc > corePoolSize,若是工做線程數超過核心線程數量 corePoolSize 而且 workQueue 不爲空,則增長工做線程。但若是超時未獲取到線程,則會把大於 corePoolSize 的線程銷燬掉。
  • timed,是 allowCoreThreadTimeOut 得來的。最終 timed 爲 true 時,則經過阻塞隊列的poll方法進行超時控制。
  • 若是在 keepAliveTime 時間內沒有獲取到任務,則返回null。若是爲false,則阻塞。

4、總結

  • 這一章節並無徹底把線程池的全部知識點都介紹完,不然一篇內容會有些臃腫。在這一章節咱們從手寫線程池開始,逐步的分析這些代碼在Java的線程池中是如何實現的,涉及到的知識點也幾乎是咱們之前介紹過的內容,包括:隊列、CAS、AQS、重入鎖、獨佔鎖等內容。因此這些知識也基本是環環相扣的,最好有一些根基不然會有些很差理解。
  • 除了本章介紹的,咱們尚未講到線程的銷燬過程、四種線程池方法的選擇和使用、以及在CPU密集型任務IO 密集型任務時該怎麼配置。另外在Spring中也有本身實現的線程池方法。這些知識點都很是貼近實際操做。
  • 好了,今天的內容先扯到這,後續的內容陸續完善。若是以上內容有錯字、流程缺失、或者很差理解以及描述錯誤,歡迎留言。互相學習、互相進步。

5、系列推薦

相關文章
相關標籤/搜索