死磕 java線程系列之線程池深入解析——生命周期

来源:https://www.cnblogs.com/tong-yuan/archive/2019/10/27/11748887.html
-Advertisement-
Play Games

(手機橫屏看源碼更方便) 註:java源碼分析部分如無特殊說明均基於 java8 版本。 註:線程池源碼部分如無特殊說明均指ThreadPoolExecutor類。 簡介 上一章我們一起重溫了下線程的生命周期(六種狀態還記得不?),但是你知不知道其實線程池也是有生命周期的呢?! 問題 (1)線程池的 ...


threadpool_life

(手機橫屏看源碼更方便)


註:java源碼分析部分如無特殊說明均基於 java8 版本。

註:線程池源碼部分如無特殊說明均指ThreadPoolExecutor類。

簡介

上一章我們一起重溫了下線程的生命周期(六種狀態還記得不?),但是你知不知道其實線程池也是有生命周期的呢?!

問題

(1)線程池的狀態有哪些?

(2)各種狀態下對於任務隊列中的任務有何影響?

先上源碼

其實,在我們講線程池體繫結構的時候,講了一些方法,比如shutDown()/shutDownNow(),它們都是與線程池的生命周期相關聯的。

我們先來看一下線程池ThreadPoolExecutor中定義的生命周期中的狀態及相關方法:

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

// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS; // 111 00000...
private static final int SHUTDOWN   =  0 << COUNT_BITS; // 000 00000...
private static final int STOP       =  1 << COUNT_BITS; // 001 00000...
private static final int TIDYING    =  2 << COUNT_BITS; // 010 00000...
private static final int TERMINATED =  3 << COUNT_BITS; // 011 00000...

// 線程池的狀態
private static int runStateOf(int c)     { return c & ~CAPACITY; }
// 線程池中工作線程的數量
private static int workerCountOf(int c)  { return c & CAPACITY; }
// 計算ctl的值,等於運行狀態“加上”線程數量
private static int ctlOf(int rs, int wc) { return rs | wc; }

從上面這段代碼,我們可以得出:

(1)線程池的狀態和工作線程的數量共同保存在控制變數ctl中,類似於AQS中的state變數,不過這裡是直接使用的AtomicInteger,這裡換成unsafe+volatile也是可以的;

(2)ctl的高三位保存運行狀態,低29位保存工作線程的數量,也就是說線程的數量最多只能有(2^29-1)個,也就是上面的CAPACITY;

(3)線程池的狀態一共有五種,分別是RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED;

(4)RUNNING,表示可接受新任務,且可執行隊列中的任務;

(5)SHUTDOWN,表示不接受新任務,但可執行隊列中的任務;

(6)STOP,表示不接受新任務,且不再執行隊列中的任務,且中斷正在執行的任務;

(7)TIDYING,所有任務已經中止,且工作線程數量為0,最後變遷到這個狀態的線程將要執行terminated()鉤子方法,只會有一個線程執行這個方法;

(8)TERMINATED,中止狀態,已經執行完terminated()鉤子方法;

流程圖

下麵我們再來看看這些狀態之間是怎麼流轉的:

threadpool_life

(1)新建線程池時,它的初始狀態為RUNNING,這個在上面定義ctl的時候可以看到;

(2)RUNNING->SHUTDOWN,執行shutdown()方法時;

(3)RUNNING->STOP,執行shutdownNow()方法時;

(4)SHUTDOWN->STOP,執行shutdownNow()方法時【本文由公從號“彤哥讀源碼”原創】;

(5)STOP->TIDYING,執行了shutdown()或者shutdownNow()後,所有任務已中止,且工作線程數量為0時,此時會執行terminated()方法;

(6)TIDYING->TERMINATED,執行完terminated()方法後;

源碼分析

你以為貼個狀態的源碼,畫個圖就結束了嘛?那肯定不能啊,下麵讓我們一起來看看源碼中是怎麼控制的。

(1)RUNNING

RUNNING,比較簡單,創建線程池的時候就會初始化ctl,而ctl初始化為RUNNING狀態,所以線程池的初始狀態就為RUNNING狀態。

// 初始狀態為RUNNING
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

(2)SHUTDOWN

執行shutdown()方法時把狀態修改為SHUTDOWN,這裡肯定會成功,因為advanceRunState()方法中是個自旋,不成功不會退出。

public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        // 修改狀態為SHUTDOWN
        advanceRunState(SHUTDOWN);
        // 標記空閑線程為中斷狀態
        interruptIdleWorkers();
        onShutdown();
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
}
private void advanceRunState(int targetState) {
    for (;;) {
        int c = ctl.get();
        // 如果狀態大於SHUTDOWN,或者修改為SHUTDOWN成功了,才會break跳出自旋
        if (runStateAtLeast(c, targetState) ||
            ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))))
            break;
    }
}

(3)STOP

執行shutdownNow()方法時,會把線程池狀態修改為STOP狀態,同時標記所有線程為中斷狀態。

public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        // 修改為STOP狀態
        advanceRunState(STOP);
        // 標記所有線程為中斷狀態
        interruptWorkers();
        tasks = drainQueue();
    } finally {
        // 【本文由公從號“彤哥讀源碼”原創】
        mainLock.unlock();
    }
    tryTerminate();
    return tasks;
}

至於線程是否響應中斷其實是在隊列的take()或poll()方法中響應的,最後會到AQS中,它們檢測到線程中斷了會拋出一個InterruptedException異常,然後getTask()中捕獲這個異常,並且在下一次的自旋時退出當前線程並減少工作線程的數量。

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // 如果狀態為STOP了,這裡會直接退出迴圈,且減少工作線程數量
        // 退出迴圈了也就相當於這個線程的生命周期結束了
        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 {
            // 真正響應中斷是在poll()方法或者take()方法中
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            // 這裡捕獲中斷異常
            timedOut = false;
        }
    }
}

這裡有一個問題,就是已經通過getTask()取出來且返回的任務怎麼辦?

實際上它們會正常執行完畢,有興趣的同學可以自己看看runWorker()這個方法,我們下一節會分析這個方法。

(4)TIDYING

當執行shutdown()或shutdownNow()之後,如果所有任務已中止,且工作線程數量為0,就會進入這個狀態。

final void tryTerminate() {
    for (;;) {
        int c = ctl.get();
        // 下麵幾種情況不會執行後續代碼
        // 1. 運行中
        // 2. 狀態的值比TIDYING還大,也就是TERMINATED
        // 3. SHUTDOWN狀態且任務隊列不為空
        if (isRunning(c) ||
            runStateAtLeast(c, TIDYING) ||
            (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
            return;
        // 工作線程數量不為0,也不會執行後續代碼
        if (workerCountOf(c) != 0) {
            // 嘗試中斷空閑的線程
            interruptIdleWorkers(ONLY_ONE);
            return;
        }

        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            // CAS修改狀態為TIDYING狀態
            if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                try {
                    // 更新成功,執行terminated鉤子方法
                    terminated();
                } finally {
                    // 強制更新狀態為TERMINATED,這裡不需要CAS了
                    ctl.set(ctlOf(TERMINATED, 0));
                    termination.signalAll();
                }
                return;
            }
        } finally {
            mainLock.unlock();
        }
        // else retry on failed CAS
    }
}

實際更新狀態為TIDYING和TERMINATED狀態的代碼都在tryTerminate()方法中,實際上tryTerminated()方法在很多地方都有調用,比如shutdown()、shutdownNow()、線程退出時,所以說幾乎每個線程最後消亡的時候都會調用tryTerminate()方法,但最後只會有一個線程真正執行到修改狀態為TIDYING的地方。

修改狀態為TIDYING後執行terminated()方法,最後修改狀態為TERMINATED,標志著線程池真正消亡了。

(5)TERMINATED

見TIDYING中分析。

彩蛋

本章我們一起從狀態定義、流程圖、源碼分析等多個角度一起學習了線程池的生命周期,你掌握的怎麼樣呢?

下一章我們將開始學習線程池執行任務的主流程,對這一塊內容感到恐懼的同學可以先看看彤哥之前寫的“手寫線程池”的兩篇文章,對接下來學習線程池的主要流程非常有好處。


歡迎關註我的公眾號“彤哥讀源碼”,查看更多源碼系列文章, 與彤哥一起暢游源碼的海洋。

qrcode


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

-Advertisement-
Play Games
更多相關文章
  • Hello,各位小伙伴大家好,我是小棧君,昨天也就是2019年10月26日,有幸在成都參加了由阿裡舉辦的“Dubbo社區開發者日”。 本次活動匯聚了各方面的大神歡聚一堂,主要是對現有微服務狀態下的技術的痛點和執行流程的分享和解析。近距離的接觸到技術大佬們,面對面的交流,讓人獲益良多。 所以小棧君這裡 ...
  • 1.什麼是rpc RPC全稱為Remote Procedure Call,翻譯過來為“遠程過程調用”。目前,主流的平臺中都支持各種遠程調用技術,以滿足分散式系統架構中不同的系統之間的遠程通信和相互調用。遠程調用的應用場景極其廣泛,實現的方式也各式各樣。 2.從通信協議的層面 基於HTTP協議的(例如 ...
  • 前言 本方法基於web2py框架,使用web2py的完整網站數據包創建簡單網站。 web2py 是一個為Python語言提供的全功能Web應用框架,旨在敏捷快速的開發Web應用,具有快速、安全以及可移植的資料庫驅動的應用,相容 Google App Engine。 (百度百科:https://bai ...
  • lambda表達式是什麼? lambda 表達式是 Python 中創建匿名函數的一個特殊語法. 我稱 lambda 語法本身為 lambda 表達式,而它返回的函數我稱之為 lambda 函數。或者稱為匿名函數。 Python 的 lambda 表達式允許在一行代碼中創建一個函數並傳遞。 看下麵的 ...
  • 相信用過 Spring Boot 的朋友們一定在啟動日誌中見過類似如下的內容,比如在啟動 Spring Boot 時,控制台預設會列印 Spring Boot Logo 以及版本信息,這是 Spring Boot 固定的還是可自定義的呢? . ____ _ __ _ _ /\\ / ___'_ __ ...
  • Python 提供了兩個基本的 socket 模塊。 第一個是 Socket,它提供了標準的 BSD Sockets API。 第二個是 SocketServer, 它提供了伺服器中心類,可以簡化網路伺服器的開發。 1、Scoket類型 套接字格式: socket(family,type[,prot ...
  • 單點登陸說明:在多個應用系統中,只需要登錄一次,就可以訪問其他相互信任的應用系統。 單點註銷說明:在多個應用系統中,只需要註銷一次,就可以註銷其他相互信任的應用系統的用戶登陸狀態。 下圖是標準單點登陸流程圖: 單點登陸與單點註銷具體實現: 1. 一共有三個相互獨立的項目,cas-server;sso ...
  • 眾所周知,線段樹是algo中很重要的一項! 一.簡介 線段樹是一種二叉搜索樹,與區間樹相似,它將一個區間劃分成一些單元區間,每個單元區間對應線段樹中的一個葉結點。 使用線段樹可以快速的查找某一個節點在若幹條線段中出現的次數,時間複雜度為O(logN)。而未優化的空間複雜度為2N,實際應用時一般還要開 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...