Java的線程

来源:https://www.cnblogs.com/feiyu2/archive/2023/05/14/thread.html
-Advertisement-
Play Games

介紹線程 線程是系統調度的最小單元,一個進程可以包含多個線程,線程是負責執行二進位指令的。 每個線程有自己的程式計數器、棧(Stack)、寄存器(Register)、本地存儲(Thread Local)等,但是會和進程內其他線程共用文件描述符、虛擬地址空間等。 對於任何一個進程來講,即便我們沒有主動 ...


介紹線程

線程是系統調度的最小單元,一個進程可以包含多個線程,線程是負責執行二進位指令的。

每個線程有自己的程式計數器、棧(Stack)、寄存器(Register)、本地存儲(Thread Local)等,但是會和進程內其他線程共用文件描述符、虛擬地址空間等。

對於任何一個進程來講,即便我們沒有主動去創建線程,進程也是預設有一個主線程的。


守護線程(Daemon Thread)

有的時候應用中需要一個長期駐留的服務程式,但是不希望這個服務程式影響應用退出,那麼我們就可以將這個服務程式設置為守護線程,如果 Java 虛擬機發現只有守護線程存在時,將結束進程。

在 Java 中將線程設置為守護線程,具體的實現代碼如下所示:

public static void main(String[] args) {
    Thread daemonThread = new Thread();
    // 必須線上程啟動之前設置
    daemonThread.setDaemon(true);
    daemonThread.start();
}

通用的線程生命周期

在操作系統層面,線程有生命周期。

對於有生命周期的事物,要學好它,只要能搞懂生命周期中各個節點的狀態轉換機制就可以了。

通用的線程生命周期基本上可以用下圖這個 “五態模型” 來描述。這五態分別是:初始狀態、可運行狀態、運行狀態、休眠狀態和終止狀態。

1651248522968-14a3c935-b45e-4ab7-bb1c-f7de1e93bf1d.png

這“五態模型”的詳細情況如下所示。


初始狀態

初始狀態,指的是線程已經被創建,但是還不允許被 CPU 調度。

初始狀態屬於編程語言特有的,這裡所謂的被創建,僅僅是在編程語言層面被創建,而在操作系統層面,真正的線程還沒有被創建。

在 Java 中,初始狀態相當於是創建了 Thread 類的對象,但是還沒有調用 Thread#start() 方法。


可運行狀態

可運行狀態,指的是線程可以被操作系統調度,但是線程還沒有開始執行。

在可運行狀態下,真正的操作系統線程已經被創建。多個線程處於可運行狀態時,操作系統會根據調度演算法選擇一個線程運行。

在 Java 中,可運行狀態相當於是調用了 Thread#start() 方法,但是線程還沒有被分配 CPU 執行。


運行狀態

當有空閑的 CPU 時,操作系統會將空閑的 CPU 分配給一個處於可運行狀態的線程,被分配到 CPU 的線程的狀態就從可運行狀態轉換成了運行狀態。

在 Java 中,運行狀態相當於是調用了 Thread#start() 方法,並且線程被分配 CPU 執行。


休眠狀態

如果運行狀態的線程調用了一個阻塞的 API(例如以阻塞的方式讀取文件)或者等待某個事件(例如條件變數),那麼線程的狀態就會從運行狀態轉換到休眠狀態,同時釋放 CPU 的使用權,休眠狀態的線程永遠沒有機會獲得 CPU 的使用權。

當等待的資源或條件滿足後,線程就會從休眠狀態轉換到可運行狀態,並等待 CPU 調度。


終止狀態

線程執行完畢或者出現異常,線程就會進入終止狀態,即線程的生命周期終止。


這五種狀態在不同編程語言里會有簡化合併。例如:

  • C 語言的 POSIX Threads 規範,就把初始狀態和可運行狀態合併了;
  • Java 程式設計語言把可運行狀態和運行狀態合併了,這兩個狀態在操作系統調度層面有用,而 Java 虛擬機層面不關心這兩個狀態,因為 Java 虛擬機把線程調度交給操作系統處理了。

除了簡化合併,這五種狀態也有可能被細化,比如,Java 語言里就細化了休眠狀態(這個下麵我們會詳細講解)。

Java 的線程生命周期

不同的程式設計語言對於操作系統線程進行了不同的封裝,下麵我們學習一下 Java 的線程生命周期。

Java 程式設計語言中,線程共有六種狀態,分別是:

  1. NEW(初始狀態)
  2. RUNNABLE(可運行 / 運行狀態)
  3. BLOCKED(阻塞狀態)
  4. WAITING(無時限等待)
  5. TIMED_WAITING(有時限等待)
  6. TERMINATED(終止狀態)

NEW(初始狀態)、TERMINATED(終止狀態)和通用的線程生命周期中的語義相同。

在操作系統層面,Java 線程中的 BLOCKED、WAITING、TIMED_WAITING 是一種狀態,即通用的線程生命周期中的休眠狀態。也就是說只要 Java 線程處於這三種狀態之一,那麼這個線程就永遠沒有機會獲得 CPU 的使用權。

所以 Java 中的線程生命周期可以簡化為下圖:

1651248522988-a3b8cd59-986b-49ee-8743-c262c7a1c180.png


其中,可以將 BLOCKED、WAITING、TIMED_WAITING 理解為導致線程處於休眠狀態的三種原因。

  • 那具體是哪些情形會導致線程從 RUNNABLE 狀態轉換到這三種狀態呢?
  • 而這三種狀態又是何時轉換回 RUNNABLE 的呢?
  • 以及 NEW、TERMINATED 和 RUNNABLE 狀態是如何轉換的?

下麵我們詳細講解。

Java 的線程狀態切換

從 NEW 到 RUNNABLE 狀態

剛創建 Thread 類的對象時,線程處於 NEW 狀態。

NEW 狀態的線程,不會被操作系統調度,因此不會執行。Java 線程要執行,就必須轉換到 RUNNABLE 狀態。

從 NEW 狀態轉換到 RUNNABLE 狀態只要調用線程對象的 start() 方法就可以了,具體的實現代碼如下所示:

public static void main(String[] args) {
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("hello");
        }
    });
    thread.start();
}

從 RUNNABLE 到 TERMINATED 狀態

線程執行完 Thrad#run() 方法後,會自動從 RUNNABLE 狀態轉換到 TERMINATED 狀態。

如果執行 run() 方法的時候異常了拋出,也會導致線程終止,進入 TERMINATED 狀態 。

1. RUNNABLE 與 BLOCKED 的狀態轉換

只有一種場景會觸發 RUNNABLE 與 BLOCKED 的狀態轉換,就是線程等待 synchronized 的隱式鎖。

  • 當使用 synchronized 申請加鎖失敗時,該線程的狀態就會從 RUNNABLE 轉換到 BLOCKED 狀態。
  • 當等待的線程獲得鎖時,該線程的狀態就會從 BLOCKED 狀態轉換到 RUNNABLE 狀態。

如果你熟悉操作系統線程的生命周期的話,可能會有個疑問:線程調用阻塞式 API 時,是否會轉換到 BLOCKED 狀態呢?在操作系統層面,線程是會轉換到休眠狀態的,但是在 Java 虛擬機層面,Java 線程的狀態不會發生變化,也就是說 Java 線程的狀態會依然保持 RUNNABLE 狀態。

Java 虛擬機層面並不關心操作系統調度相關的狀態,因為在 Java 虛擬機看來,等待 CPU 的使用權(操作系統層面此時處於可執行狀態)與等待 I/O(操作系統層面此時處於休眠狀態)沒有區別,都是在等待某個資源,所以都歸入了 RUNNABLE 狀態。

而我們說的 Java 線程在調用阻塞式 API 時,線程會阻塞,指的是操作系統線程的狀態,並不是 Java 線程的狀態。

2. RUNNABLE 與 WAITING 的狀態轉換

總體來說,有三種場景會觸發 RUNNABLE 與 WAITING 的狀態轉換。


第一種場景,獲得 synchronized 隱式鎖的線程,調用無參數的 Object#wait() 方法。

這裡應該調用的是鎖對象的 wait() 方法,具體的實現代碼如下所示:

public void method() throws InterruptedException {
    synchronized (this) {
        this.wait();
    }
}
  • 當調用 wait() 方法時,調用方法的線程的狀態從 RUNNABLE 狀態轉換到 WAITING 狀態
  • 當調用 notify() 方法時,被喚醒的線程的狀態從 WAITING 狀態轉換到 RUNNABLE 狀態

第二種場景,調用無參數的 Thread#join() 方法。

join() 是一種線程同步方法,例如有一個線程對象 thread A:

  • 當調用 A.join() 方法時,執行這條語句的線程會等待 thread A 執行完,而等待中的這個線程,其狀態會從 RUNNABLE 轉換到 WAITING。
  • 當線程 thread A 執行完,原來等待它的線程又會從 WAITING 狀態轉換到 RUNNABLE。

Thread#join() 方法的實現基於 Object#wait()。


第三種場景,調用 LockSupport#park() 方法。

LockSupport 類,也許你有點陌生,其實 Java 併發包中鎖的實現都用到了 LockSupport#park() / unpark()。

  • 當調用 LockSupport.park() 方法時,調用方法的線程的狀態從 RUNNABLE 轉換到 WAITING。
  • 當調用 LockSupport.unpark(Thread thread) 方法時,被喚醒的線程的狀態從 WAITING 狀態轉換到 RUNNABLE 狀態

總結來說:Object#wait() 和 LockSupport#park() 方法使線程的狀態轉換到 WAITING。

3. RUNNABLE 與 TIMED_WAITING 的狀態轉換

總體來說,有五種場景會觸發 RUNNABLE 與 TIMED_WAITING 的狀態轉換:

  1. 獲得 synchronized 隱式鎖的線程,調用帶超時參數的 Object#wait(long timeout) 方法;
  2. 調用帶超時參數的 Thread#join(long millis) 方法;(底層調用 Object#wait(long timeout) )
  3. 調用帶超時參數的 LockSupport.parkNanos(Object blocker, long deadline) 方法;
  4. 調用帶超時參數的 LockSupport.parkUntil(long deadline) 方法。
  5. 調用帶超時參數的 Thread.sleep(long millis) 方法;

這裡你會發現:

  • TIMED_WAITING 和 WAITING 狀態的區別,僅僅是觸發條件多了超時參數。
  • 與 RUNNABLE 與 WAITING 的狀態轉換 相比,多了一個 Thread.sleep() 場景。

Java 線程 API 的使用

線程的創建

創建線程的幾種方式:

  1. 繼承 Thread 類,重寫 run() 方法。
  2. 實現 Runnable 介面,實現其中的 run() 方法。將該實現類的對象作為參數傳遞到 Thread 類的構造器中,創建 Thread 類的對象。
  3. 實現 Callable 介面,實現其中的 call() 方法。將該實現類的對象作為參數傳遞到 FutureTask 類的構造器中,創建FutureTask 類的對象。將 FutureTask 類的對象作為參數傳遞到 Thread 類的構造器中,創建 Thread 類的對象。Callable 它解決了 Runnable 無法返回結果的困擾。

「實現 Runnable 介面」VS「繼承 Thread 類」

  • 通過實現(implements)的方式沒有類的單繼承性的局限性
  • 實現的方式更適合處理多個線程有共用數據的情況

「實現 Callable 介面」VS「實現 Runnable 介面」

  • call() 可以有返回值
  • call() 可以拋出異常被外面的操作捕獲,獲取異常的信息
  • 「實現 Callable 介面」支持泛型

// 自定義線程對象
class MyThread extends Thread {
    public void run() {
        // 線程需要執行的代碼
        ......
    }
}

// 創建線程對象
MyThread myThread = new MyThread();
// 實現Runnable介面
class Runner implements Runnable {
    @Override
    public void run() {
        // 線程需要執行的代碼
        ......
    }
}

// 創建線程對象
Thread thread = new Thread(new Runner());
public static void main(String[] args) throws ExecutionException, InterruptedException {
    MyTask task = new MyTask();
    // FutureTask 用於接收運算結果
    FutureTask futureTask = new FutureTask<>(task);
    Thread thread = new Thread(futureTask);

    thread.start();
	// FutureTask 可用於線程間同步 (當前線程等待其他線程執行完成之後,當前線程才繼續執行)
    // get() 返回值即為 FutureTask 構造器參數 Callable 實現類實現的 call() 的返回值
    System.out.println(futureTask.get());
}

public class MyTask implements Callable {
    @Override
    public String call() {
        // 若不需要返回值,可 return null;
        return "ok";
    }
}

線程的執行

創建好 Thread 類的對象後,通過調用 Thread#start() 方法創建線程執行任務。

線程執行要調用 start() 而不是直接調用 run(),直接調用 run() 方法只會在當前線程上同步執行 run() 方法的內容,而不會啟動新線程。調用 start() 方法的作用:

  1. 啟動一個新的線程
  2. 新的線程調用 run() 方法

線程的停止

有時候我們需要強制中斷 run() 方法的執行,例如 run() 方法訪問一個很慢的網路,我們等不下去了,想終止怎麼辦呢?Java 的 Thread 類裡面倒是有個 stop() 方法,不過已經標記為 @Deprecated,所以不建議使用了。正確的方式是調用 interrupt() 方法。Thread#interrupt() 配合合適的代碼,即可優雅的實現線程的終止。

stop() 和 interrupt() 方法的區別。

  • stop() 方法會真的殺死線程,不給線程喘息的機會,如果線程持有 ReentrantLock 鎖,被 stop() 的線程並不會自動調用 ReentrantLock 的 unlock() 去釋放鎖,那其他線程就再也沒機會獲得 ReentrantLock 鎖,這實在是太危險了。所以該方法就不建議使用了,類似的方法還有 suspend() 和 resume() 方法,這兩個方法同樣也都不建議使用了。
  • interrupt() 方法僅僅是通知線程,線程有機會執行一些後續操作,線程也可以無視這個通知。被 interrupt 的線程,是怎麼收到通知的呢?一種是異常,另一種是主動檢測。

異常

當線程 A 處於 WAITING、TIMED_WAITING 狀態時,如果其他的線程調用線程 A 的 interrupt() 方法,會使線程 A 返回到 RUNNABLE 狀態,同時線程 A 的代碼會觸發 InterruptedException 異常。

上面我們提到轉換到 WAITING、TIMED_WAITING 狀態的觸發條件,都是調用了類似 wait()、join()、sleep() 這樣的方法,我們看這些方法的簽名,發現都會 throws InterruptedException 這個異常。這個異常的觸發條件就是:其他的線程調用了該線程的 interrupt() 方法。

當線程 A 處於 RUNNABLE 狀態時:

  • 當線程 A 處於 RUNNABLE 狀態,並且阻塞在 java.nio.channels.InterruptibleChannel 上時,如果其他的線程調用線程 A 的 interrupt() 方法,線程 A 會觸發 java.nio.channels.ClosedByInterruptException 這個異常;
  • 當線程 A 處於 RUNNABLE 狀態,並且阻塞在 java.nio.channels.Selector 上時,如果其他的線程調用線程 A 的 interrupt() 方法,線程 A 的 java.nio.channels.Selector 會立即返回。

上面這兩種情況屬於被中斷的線程通過異常的方式獲得了通知。


主動檢測

還有一種是主動檢測,如果線程處於 RUNNABLE 狀態,並且沒有阻塞在某個 I/O 操作上,例如中斷計算圓周率的線程 A,這時就得依賴線程 A 主動檢測中斷狀態了。如果其他的線程調用線程 A 的 interrupt() 方法,那麼線程 A 可以通過 isInterrupted() 方法,檢測是不是自己被中斷了。

參考資料

第17講 | 一個線程兩次調用start()方法會出現什麼情況?-極客時間 (geekbang.org)

09 | Java線程(上):Java線程的生命周期 (geekbang.org)

06 | 線程池基礎:如何用線程池設計出更“優美”的代碼? (geekbang.org)

11 | 線程:如何讓複雜的項目並行執行?-極客時間 (geekbang.org)

本文來自博客園,作者:真正的飛魚,轉載請註明原文鏈接:https://www.cnblogs.com/feiyu2/p/thread.html


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

-Advertisement-
Play Games
更多相關文章
  • 0 文章概述 大家想一想工作中有沒有遇到以下情況:一位同事用了很長時間羅列了很多事實和數據向你說明一件事情,但是你聽完根本不知道他想要說什麼。一位同事用了大量筆墨編寫了技術方案,不僅有文字還有圖表,但是你看完也不知道這個方案到底要解決什麼問題以及如何落地。 上述情況的出現大概率是因為表述者沒有使用結 ...
  • DDD作為架構設計思想幫助微服務控制規模複雜度,那它是怎麼做到的呢? 一、架構設計是為瞭解決系統複雜度 談到架構,相信每個技術人員都是耳熟能詳,但如果深入探討一下,“為何要做架構設計?”或者“架構設計目的是什麼?”類似的問題,大部分人可能從來沒有思考過,或者即使有思考,也沒有太明確可信的答案。 1. ...
  • 本文主要講解了京東百億級商品車型適配數據存儲結構設計以及怎樣實現適配介面的高性能查詢。通過京東百億級數據緩存架構設計實踐案例,簡單剖析了jimdb的點陣圖(bitmap)函數和lua腳本應用在高性能場景。希望通過本文,讀者可以對緩存的內部結構知識有一定瞭解,並且能夠以最小的記憶體使用代價將點陣圖(bitm... ...
  • requires 是 C++20 中引入的一個新關鍵字,用於在函數模板或類模板中聲明所需的一組語義要求,它可以用來限制模板參數,類似於 typename 和 class 關鍵字。 requires關鍵字常與type_traits頭文件下類型檢查函數匹配使用,當requires後的表達式值為true時 ...
  • 1. 配置文件的兩種寫法:properties 和 yml 2. 項目中存在多個配置文件,可以使用 spring.profiles.active 屬性來切換使用哪個配置文件。 3. 自定義的一些配置屬性(配置項),如何讀取呢?可以在程式中通過 @Value 或者 @ConfigurationPr... ...
  • 歡迎來到我們的系列博客《Python360全景》!在這個系列中,我們將帶領你從Python的基礎知識開始,一步步深入到高級話題,幫助你掌握這門強大而靈活的編程語法。無論你是編程新手,還是有一定基礎的開發者,這個系列都將提供你需要的知識和技能。這是我們的第一篇文章,讓我們從最基礎的開始:如何在你的電腦... ...
  • 1、什麼是Spring Cloud ? Spring cloud 流應用程式啟動器是基於 Spring Boot 的 Spring 集成應用程式,提供與外部系統的集成。Spring cloud Task,一個生命周期短暫的微服務框架,用於快速構建執行有限數據處理的應用程式。 Spring Cloud ...
  • 線程阻塞概述 在生活中,最常見的阻塞現象是公路上汽車的堵塞。汽車在公路上快速行駛,如果前方交通受阻,就只好停下來等待,等到公路順暢,才能恢復行駛。 線程在運行中也會因為某些原因而阻塞。所有處於阻塞狀態的線程的共同特征:放棄 CPU,暫停運行,只有等到導致阻塞的原因消除,才能恢復運行,或者被其他線程中 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...