java線程池原理解析

来源:https://www.cnblogs.com/floor/archive/2020/05/08/12853748.html
-Advertisement-
Play Games

五一假期大雄看了一本《java併發編程藝術》,瞭解了線程池的基本工作流程,竟然發現線程池工作原理和互聯網公司運作模式十分相似。 線程池處理流程 原理解析 互聯網公司與線程池的關係 這裡用一個比喻來描述一下線程池,中間有一些名詞你可能不是太清楚,後邊源碼解析的部分會講到。 你可以把 線程池 看作是一個 ...


五一假期大雄看了一本《java併發編程藝術》,瞭解了線程池的基本工作流程,竟然發現線程池工作原理和互聯網公司運作模式十分相似。

線程池處理流程

線程池任務執行流程

原理解析

互聯網公司與線程池的關係

這裡用一個比喻來描述一下線程池,中間有一些名詞你可能不是太清楚,後邊源碼解析的部分會講到。

你可以把線程池看作是一個研發部門,研發部門有很多程式員(Worker), 他們在一個大辦公室里(HashSet workers)。程式員乾不完的需求(Runnable/Callable)放在需求池(workQueue)里排隊。每個研發部都配置有骨幹程式員數量(corePoolSize)最大能容納的程式員數量(maximumPoolSize)。具體要做的任務就是產品的需求

new 一個線程池相當於創建了一個研發部,創建研發部時需要指定骨幹程式員數量,最大能容納的程式員數量,需求池用哪種(BlockingQueue),如果忙不過來的需求怎麼給產品回覆(拒絕策略)等等內容。剛開始這個研發部一個程式員也沒有。

當產品給這個研發部提一個需求時(當然肯定不會只提一個,他們會不斷的提需求。這裡以提一個需求為例)

首先會看骨幹程式員招聘滿了沒。

如果沒滿,會招聘一個骨幹程式員,招聘進來就讓他不停的工作(很殘酷啊),幹完剛派過來的任務他會主動在需求池找下一個需求來做(好員工),如果需求池沒有需求了,他就停止工作了,然後研發部會把他裁掉,如果裁掉後發現骨幹程式員數量不夠了,就會再招聘一個程式員。裁掉後,要是骨幹程式員數量還夠就不招聘了。

如果骨幹程式員數量滿了,就看需求池滿沒滿,如果需求池沒滿,就把需求扔進需求池裡;如果需求池滿了,就看程式員數量有沒有達到上限,如果達到了,就對產品說,這個需求我們做不了,沒資源;如果沒達到,就招聘一個程式員,招聘進來就讓他不停的工作,幹完剛派過來的需求他會主動到需求池找下一個任務來做,如果需求池沒有任務了,他就停止工作了,然後研發部會把他裁掉,如果裁掉後發現骨幹程式員數量不夠了,就會再招聘一個程式員。裁掉後,要是骨幹程式員數量還夠就不招聘了。

源碼解析

首先是worker(程式員)

Worker被裝在一個HashSet(workers)裡邊, 他是用來執行任務的,他們的職責就是不斷的從workQueue裡邊取任務,然後執行。當workQueue(需求池)裡邊拿不到任務,或者線程池達到特定狀態,worker就會從workers裡邊移走(被裁)。

下邊是Worker源碼,移除了非關鍵的東西

private final class Worker
    extends AbstractQueuedSynchronizer
    implements Runnable
{

    // 標識這個任務是在哪個線程運行
    final Thread thread;
    Runnable firstTask;
    // 完成了幾個任務
    volatile long completedTasks;

    Worker(Runnable firstTask) {
        // 阻止中斷,知道runWorker執行
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        // 直接用你提供的線程工廠搞個線程出來
        this.thread = getThreadFactory().newThread(this);
    }

    // 調用ThreadPoolExecutor裡邊的runWorker方法
    public void run() {
        runWorker(this);
    }

    // 以下這些是AQS相關的東西

    // 0代表沒有加鎖
    // 1代表加鎖了
    protected boolean isHeldExclusively() {
        return getState() != 0;
    }

    protected boolean tryAcquire(int unused) {
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }

    protected boolean tryRelease(int unused) {
        setExclusiveOwnerThread(null);
        setState(0);
        return true;
    }

    public void lock()        { acquire(1); }
    public boolean tryLock()  { return tryAcquire(1); }
    public void unlock()      { release(1); }
    public boolean isLocked() { return isHeldExclusively(); }

    void interruptIfStarted() {
        Thread t;
        if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
            try {
                t.interrupt();
            } catch (SecurityException ignore) {
            }
        }
    }
}

Worker實現了Runnable介面,所以他是個任務,有run方法;同時有繼承了AQS,所以他也是一把鎖。

下邊是提交任務的過程

提交任務有submit和execute, submit就是首先將Callable或者Runnable包裝成FutureTask,然後調用execute, 所以核心是分析execute

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    // 這個c裡邊有兩個信息,一個是現在有多少worker, 另一個是現線上程池的狀態是啥
    // workerCountOf方法就是從裡邊提取 worker的數量的
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) { // 當前worker的數量比需要的核心線程數少
        // 加worker去執行,加成功就完事了,也就是說只要worker比核心線程數少,就會創建worker
        // 不管現在核心線程是否在工作,也不管workQueue是不是滿的
        // addWorker的第二個參數表示是不是要加核心線程(或者叫核心worker)
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 當前worker達到或超過了核心線程數,或者加worker失敗了,才會走下邊的流程
    // worker已經比核心線程數多了

    // 如果 線程池沒有shutdown的話 
    // 就嘗試將任務加到workQueue裡邊,工作隊列入隊成功的話再往裡邊走
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            // 再次檢查狀態如果線程池要停了,那麼就拒絕任務,並且把worker從工作隊列扔掉
            reject(command);
        else if (workerCountOf(recheck) == 0)
            // 如果沒有worker的話(說明沒加進去,這種場景我沒想到是什麼情況),加一個worker
            addWorker(null, false);
        // 其他情況,丟到工作隊列就不用管了,等著worker去處理
    }
    // 如果隊列滿了加失敗了,或者線程池狀態不滿足了,就嘗試加普通worker(非核心線程)
    else if (!addWorker(command, false))
        // 加失敗了就拒絕任務
        // 失敗一方面可能是worker數量已經達到你的給的maximumPoolSize
        // 另一方面,可能是檢查到線程池的狀態不對了
        reject(command);
}

可以發現execute方法就是完成了上邊說的“線程池處理流程”這個圖裡描述的過程。 大雄看到這裡還有幾個疑問,一個是Woker是如何創建並加入workers的,一個是worker是如何啟動的,再就是worker是如何運行的

生活還要繼續

private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // 做一些校驗,線程池的狀態要滿足一定條件
        // 而且得提交任務過來,再就是workQueue不能是空的
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
                firstTask == null &&
                ! workQueue.isEmpty()))
            return false;

        for (;;) {
            int wc = workerCountOf(c);
            // 看你是要創建核心worker還是普通worker
            // 核心看超沒超過corePoolSize, 普通看超沒超過maximumPoolSize
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            if (compareAndIncrementWorkerCount(c))
            // 增加worker數量失敗就在來
                break retry;
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)
                // 中途線程池狀態發生變化了
                continue retry;
        }
    }

    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        // worker就是這麼創建的
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            // 加worker是要加全局鎖的
            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) {
                // worker是在這裡啟動的
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

這段代碼解決了 Woker是如何創建並加入workers的以及worker是如何啟動的的問題。

addWorker做的核心工作就是,創建worker, 啟動worker, 在創建之前還會做一些校驗。調用了worker裡邊線程的start後就要等待cpu調度執行worker的run方法了。

public void run() {
    runWorker(this);
}

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        // task是創建worker帶進去的任務,會先執行他,然後從workQueue裡邊取
        // 如果沒有的話跳出去
        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, 預設是空的
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    // 這個跟我們平時理解的Runnable還不一樣,可以體會下,他這個run就是一個普通的方法
                    // 他直接調run是要執行任務,線程的start只是把worker裡邊的那個run跑起來了
                    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 {
        // 從while跳出來表明沒有任務可以執行了
        processWorkerExit(w, completedAbruptly);
    }
}

這個也比較容易,就是不斷的從workQueue取任務,執行,直到沒任務了跳出來。接下來就是worker如何被銷毀的問題了

private void processWorkerExit(Worker w, boolean completedAbruptly) {
    if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
        decrementWorkerCount();

    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        completedTaskCount += w.completedTasks;
        // 移除掉worker(裁員)
        workers.remove(w);
    } finally {
        mainLock.unlock();
    }

    tryTerminate();

    int c = ctl.get();
    if (runStateLessThan(c, STOP)) {
        if (!completedAbruptly) {
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            if (min == 0 && ! workQueue.isEmpty())
                min = 1;
            // 比核心線程數多的話,執行完的Worker直接移除就好
            if (workerCountOf(c) >= min)
                return; // replacement not needed
        }
        // 小於核心線程數就會再加個Worker, 讓他繼續等待接收任務(招人)
        addWorker(null, false);
    }
}

直接從workers裡邊移除worker, 移除後如果worker數量比核心線程數還少,就再加個worker, 否則不加。

一些體會

看源碼一定不要過分糾結細節,就像這個線程池,我看網上很多文章去算那幾個位運算的十進位數,感覺是在浪費時間,沒有抓住重點。

當然這也不是絕對的(似乎說的矛盾了),一些細節的設計還是非常精妙值得學習的。還是這個位運算,為什麼只用一個int表示線程池狀態和worker的數量呢。

要多多聯想,還是這個位運算,他是不是和讀寫鎖用一個int既表示寫狀態又表示讀狀態十分相似。Worker繼承AQS,是否能讓你想起AQS的種種。

總之,個人覺得第一遍看是一定不能沉溺於細節的,他會讓你迷惘和喪失信心;第二遍、第三遍可以關註一下細節,感受大師級的設計的美妙之處。當然筆者僅僅粗略看了一遍(逃~)

最後

大雄五一假期閱讀了《java併發編程藝術》這本書,整理了一本gitbook筆記(還沒寫完),需要的同學可以掃描文末二維碼關註“大雄和你一起學編程”公眾號,後臺回覆我愛java領取。這本gitbook還沒徹底完成,所以可能還有些小錯誤。未來會大約每兩天推送其中的一篇文章。

如下是這本gitbook的目錄截圖

筆記目錄截圖

歡迎關註“大雄和你一起學編程”公眾號


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • SPI服務發現機制 SPI是Java JDK內部提供的一種服務發現機制。 SPI Service Provider Interface,服務提供介面,是Java JDK內置的一種服務發現機制 通過在ClassPath路徑下的META INF/services文件夾查找文件,自動載入文件里所定義的類 ...
  • 軟體設計七大原則 1、 開閉原則 定義:一個軟體實體,如類、模塊和函數應該對擴展開放,對修改關閉。中心思想:用抽象構建框架,用實現擴展細節。即面向抽象編程。優點:提高軟體系統的可復用性和可維護性。舉例:很多互聯網公司實行彈性制考勤,每天上班8小時,這是不可修改的,但是什麼時間上班和下班,是開放的。 ...
  • Spring Cloud Alibaba各組件版本關係 | Spring Cloud Alibaba Version | Sentinel Version | Nacos Version | RocketMQ Version | Dubbo Version | Seata Version | | | ...
  • 1、更改docker鏡像倉庫 2、可以先查看有哪些容器了 3、查看正在運行和沒有運行的容器 4、從中央倉庫拉取鏡像 5、後臺運行容器並映射配置文件 6、查看是否啟動成功 docker ps 根據自己的IP加上9200埠訪問即可 此時有可能會失敗,可以使用 docker logs 查看運行日誌 拿去 ...
  • Lippman在《深度探索C++對象模型》的前言中寫道: I have heard a number of people over the years voice opinions similar to those of your colleagues. In every case, those o ...
  • 1 簡介 是面向對象的編程語言,只要使用它,就需要創建對象。Java創建對象有六種方法,實際常用的不會這麼多,這裡權當是記錄一下。 2 六種方法 (1)使用new關鍵字 (2)反射之Class類newInstance() (3)反射之Constructor類的newInstance() (4)Obj ...
  • 問題 設計一個person類,條件如下: 1)定義protected屬性:name(姓名)、age(年齡)、sex(性別) 2)定義static靜態屬性:num(用於計算已實例化的人數) 3)定義構造函數,實現在對象創建時輸出“I am a person”,並對num加1; 4)定義析構函數,在對象 ...
  • `Apache Dubbo Java RPC` 框架,它提供了三大核心能力:面向介面的遠程方法調用,智能容錯和負載均衡,以及服務自動註冊和發現。 註意,是 ,不再是 。簡單來說就是 將 移交給 開源社區進行維護。參見 "dubbo spring boot project" "Spring Boot ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...