Java 併發編程解析 | 關於Java領域中的線程機制,我們應該知道的那些事?

来源:https://www.cnblogs.com/mazhilin/archive/2022/08/02/16543385.html
-Advertisement-
Play Games

蒼穹之邊,浩瀚之摯,眰恦之美; 悟心悟性,善始善終,惟善惟道! —— 朝槿《朝槿兮年說》 寫在開頭 我們都知道,經過多年的發展和無數Java開發者的不懈努力,Java已經由一門單純的電腦編程語言,逐漸演變成一套強大的以及仍在可持續發展中的技術體系平臺。 雖然,Java設計者們根據不同的技術規範,把 ...


蒼穹之邊,浩瀚之摯,眰恦之美; 悟心悟性,善始善終,惟善惟道! —— 朝槿《朝槿兮年說》

寫在開頭

我們都知道,經過多年的發展和無數Java開發者的不懈努力,Java已經由一門單純的電腦編程語言,逐漸演變成一套強大的以及仍在可持續發展中的技術體系平臺。

雖然,Java設計者們根據不同的技術規範,把Java劃分為3種結構獨立且又彼此依賴的技術體系,分別是Java SE,Java EE 以及Java ME,其中Java EE 在廣泛應用在企業級開發領域中。

除了包括Java API組件外,其衍生和擴充了Web組件,事務組件,分散式組件,EJB組件,消息組件等,並且持續發展到如今,其中,雖然有許多組件現如今不再適用,但是許多組件在我們日常開發工作中,扮演著同樣重要的角色和依舊服務著我們日新月異的業務需求。

綜合Java EE的這些技術,我們可以根據我們的實際需要和滿足我們的業務需求的情況下,可以快速構建出一個具備高性能,結構嚴謹且相對穩定的應用平臺,雖然現在雲原生時代異軍突起許多基於非Java的其他技術平臺,但是在分散式時代,Java EE是用於構建SOA架構的首先平臺,甚至基於SpringCloud構建微服務應用平臺也離不開Java EE 的支撐。

個人覺得,Java的持續發展需要感謝Google,正是起初Google將Java作為Android操作系統的應用層編程語言,使得Java可以在PC時代和移動互聯網時代得到快速發展,可以用於手持設備,嵌入式設備,個人PC設備,高性能的集群伺服器和大型機器平臺。

當然,Java的發展也不是一帆風順的,也曾被許多開發者詬病和嫌棄,但是就憑Java在行業里能否覆蓋的場景來說,對於它的友好性和包容性,這不由讓我們心懷敬意。其中,除了Java有豐富的內置API供我們使用外,尤其Java對於併發編程的支持,也是我們最難以釋懷的,甚至是我們作為Java開發者最頭疼的問題所在。

雖然,併發編程這個技術領域已經發展了半個世紀了,相關的理論和技術紛繁複雜。那有沒有一種核心技術可以很方便地解決我們的併發問題呢?今天,我們就來一起走進Java領域的併發編程的核心——Java線程機制。

基本概述

在Java中,對於Java語言層面的線程,我們基本都不會太陌生,甚至耳熟能詳。但是在此之前,我們先來探討一下,什麼是管程技術?Java 語言在 1.5 之前,提供的唯一的併發原語就是管程,而且 1.5 之後提供的 SDK 併發包,也是以管程技術為基礎的。除此之外,其中C/C++、C# 等高級語言也都支持管程。

關於管程

管程(Monitor)是指定義了一個數據結構和能為併發進程所執行的一組操作,這組操作能同步進程和改變管程中的數據。主要是指提供了一種機制,線程可以臨時放棄互斥訪問,等待某些條件得到滿足後,重新獲得執行權恢復它的互斥訪問。

所謂管程,指的是管理共用變數以及對共用變數的操作過程,讓他們支持併發。翻譯為 Java 領域的語言,就是管理類的成員變數和成員方法,讓這個類是線程安全的。

基本定義

首先,系統中的各種硬體資源和軟體資源均可用數據結構抽象地描述其資源特性,即用少量信息和對該資源所執行的操作來表徵該資源,而忽略它們的內部結構和實現細節。

其次,可以利用共用數據結構抽象地表示系統中的共用資源,並且將對該共用數據結構實施的特定操作定義為一組過程。進程對共用資源的申請、釋放和其它操作必須通過這組過程,間接地對共用數據結構實現操作。

然後,對於請求訪問共用資源的諸多併發進程,可以根據資源的情況接受或阻塞,確保每次僅有一個進程進入管程,執行這組過程,使用共用資源,達到對共用資源所有訪問的統一管理,有效地實現進程互斥。

最後,代表共用資源的數據結構以及由對該共用數據結構實施操作的一組過程所組成的資源管理程式共同構成了一個操作系統的資源管理模塊,我們稱之為管程,管程被請求和釋放資源的進程所調用。

綜上所述,管程(Monitor)是指定義了一個數據結構和能為併發進程所執行的一組操作,這組操作能同步進程和改變管程中的數據。主要是指提供了一種機制,線程可以臨時放棄互斥訪問,等待某些條件得到滿足後,重新獲得執行權恢復它的互斥訪問。

基本組成

由上述的定義可知,管程由四部分組成:

  • 管程的名稱;
  • 局部於管程的共用數據結構說明;
  • 對該數據結構進行操作的一組過程;
  • 對局部於管程的共用數據設置初始值的語句

實際上,管程中包含了面向對象的思想,它將表徵共用資源的數據結構及其對數據結構操作的一組過程,包括同步機制,都集中並封裝在一個對象內部,隱藏了實現細節。

封裝於管程內部的數據結構僅能被封裝於管程內部的過程所訪問,任何管程外的過程都不能訪問它;反之,封裝於管程內部的過程也僅能訪問管程內的數據結構。

所有進程要訪問臨界資源時,都只能通過管程間接訪問,而管程每次只准許一個進程進入管程,執行管程內的過程,從而實現了進程互斥。

基本特點

管程是一種程式設計語言的結構成分,它和信號量有同等的表達能力,從語言的角度看,管程主要有以下特點:

  • 模塊化,即管程是一個基本程式單位,可以單獨編譯;
  • 抽象數據類型,指管程中不僅有數據,而且有對數據的操作;
  • 信息屏蔽,指管程中的數據結構只能被管程中的過程訪問,這些過程也是在管程內部定義的,供管程外的進程調用,而管程中的數據結構以及過程(函數)的具體實現外部不可見。
基本模型

在管程的發展史上,先後出現過三種不同的管程模型,分別是:Hasen 模型、Hoare 模型和 MESA 模型。其中,現在廣泛應用的是 MESA 模型,並且 Java 管程的實現參考的也是 MESA 模型。

接下來,我們就針對幾種管程模型分別來簡單的說明一下,它們之間的區別。

假設有這樣一個進程同步機制中的問題:如果進程P1因x條件處於阻塞狀態,那麼當進程P2執行了x.signal操作喚醒P1後,進程P1和P2此時同時處於管程中了,這是不被允許的,那麼如何確定哪個執行哪個等待?

一般來說,我們都會採用如下兩種方式來進行處理:

  • 第一種方式:假如進程 P2進行等待,直至進程P1離開管程或者等待另一個條件
  • 第二種方式:假如進程 P1進行等待,直至進程P2離開管程或者等待另一個條件

綜上所述,三種不同的管程模型採取的方式如下:

1.Hasen 模型

Hansan管程模型,採用了基於兩種的折中處理。主要是規定管程中的所有過程執行的signal操作是過程體的最後一個操作,於是,進程P2執行完signal操作後立即退出管程,因此進程P1馬上被恢復執行。

2.Hoare 模型

Hoare 管程模型,採用第一種方式處理。只要進程 P2進行等待,直至進程P1離開管程或者等待。

3.MESA 模型

MESA 管程模型,採用第二種方式處理。只要進程 P1進行等待,直至進程P2離開管程或者等待。

基本實現

在併發編程領域,有兩大核心問題:互斥和同步。其中:

  • 互斥(Mutual Exclusion),即同一時刻只允許一個線程訪問共用資源
  • 同步(Synchronization),即線程之間如何通信、協作

這兩大問題,管程都是能夠解決的。主要是由於信號量機制是一種進程同步機制,但每個要訪問臨界資源的進程都必須自備同步操作wait(S)和signal(S)。

這樣大量同步操作分散到各個進程中,可能會導致系統管理問題和死鎖,在解決上述問題的過程中,便產生了新的進程同步工具——管程。其中:

  • 信號量(Semaphere):操作系統提供的一種協調共用資源訪問的方法。和用軟體實現的同步比較,軟體同步是平等線程間的的一種同步協商機制,不能保證原子性。而信號量則由操作系統進行管理,地位高於進程,操作系統保證信號量的原子性。

  • 管程(Monitor):解決信號量在臨界區的 PV 操作上的配對的麻煩,把配對的 PV 操作集中在一起,生成的一種併發編程方法。其中使用了條件變數這種同步機制。

綜上所述,這也是Java中,最常見的鎖機制的實現方案,即最典型的實現就是ReenTrantLock為互斥鎖(Mutex Lock) 和synchronized 為同步鎖(Synchronization Lock)。

具體表現

熟悉Java中synchronized 關鍵詞的都應該知道,它是Java語言為開發者提供的同步工具,主要用來解決多線程併發執行過程中數據同步的問題,主要有wait()、notify()、notifyAll() 這三個方法。其中,最關鍵的實現是,當我們在代碼中聲明synchronized 之後,其被聲明部分代碼編譯之後會生成一對monitorenter和monitorexit指令來指定某個同步塊。

在JVM執行指令過程中,一般當遇到monitorenter指令表示獲取互斥鎖時,而當遇到monitorexit指令表示要釋放互斥鎖,這就是synchronized在Java層面實現同步機制的過程。除此之外,如果是獲取鎖失敗,則會將當前線程放入到阻塞讀隊列中,當其他線程釋放鎖時,再通知阻塞讀隊列中的線程去獲取鎖。

由此可見,我們可以知道的是,synchronized 代碼塊是由一對 monitorenter/monitorexit 指令實現的,Monitor 對象是同步的基本實現單元。

準確的說,JVM一般通過Monitor來實現monitorenter和monitorexit指令,而且Monitor 對象包括一個阻塞隊列和一個等待隊列。其中,阻塞隊列用來保存鎖競爭失敗的線程,並且它處於阻塞狀態,而等待隊列則用來保存synchronized 代碼塊中調用wait方法後放置的隊列,其調用wait方法後會通知阻塞隊列。

當然,在 Java 6 之前,Monitor 的實現完全是依靠操作系統內部的互斥鎖,因為需要進行用戶態到內核態的切換,所以同步操作是一個無差別的重量級操作。

這並不意味著,Java是提供信號量這種編程原語來支持解決併發問題的,雖然在《操作系統原理》中,我們知道用信號量能解決所有併發問題,但是在Java中並不是這樣的。

其實,最根本的原因,就是Java 採用的是管程技術,synchronized 關鍵字及 wait()、notify()、notifyAll() 這三個方法都是管程的組成部分。而管程和信號量是等價的,所謂等價指的是用管程能夠實現信號量,也能用信號量實現管程。

特別指出的是,相對於synchronized來說,ReentrantLock主要有以下幾個特點:

  • 從鎖獲取粒度上來看,比synchronized較為細,主要表現在是鎖的持有是以線程為單位而不是基於調用次數。
  • 從線程公平性上來看,ReentrantLock 可以設置公平性(fairness),能減少線程“饑餓”的發生。
  • 從使用角度上來看,ReentrantLock 可以像普通對象一樣使用,所以可以利用其提供的各種便利方法,進行精細的同步操作,甚至是實現 synchronized 難以表達的用例。
  • 從性能角度上來看,synchronized 早期的實現比較低效,對比 ReentrantLock,大多數場景性能都相差較大。雖然在 Java 6之後 中對其進行了非常多的改進,但在高競爭情況下,ReentrantLock 仍然有一定優勢。

綜上所述,我我相信你對Java中的管程技術已經有了一個明確的認識。接下來,我們便來進入今天的主題——Java線程機制。

關於線程

在早期的操作系統中,執行任務被抽象為進程(Process)。其中,進程是操作系統運行和調度的基本單元。

隨著電腦技術的不斷發展,由於進程開銷資源較大,以進程為調度單位的方式逐漸產生弊端。因此,電腦先進工作者(科學家)們在進程的基礎上,提出了線程(Thead)的概念。

線程是進程中的運行單位,可以把線程看作輕量級的進程。電腦CPU會按照某種策略為每一個線程分配一定的時間片去執行。

進程是指程式的一次動態執行過程,電腦中正在執行的程式就是進程,每一個程式都對對應著各自的一個進程。

一個進程包含了從代碼載入完畢到執行完成的一個完成過程,是操作系統中資源分配的最小單位。

線程是比進程更小的執行單元,是電腦CPU調度和分配的基本單位。

每一個進程都會至少包含一個線程,而一個線程只屬於一個進程。

每一個進程都有自己的資源,一個進程內的所有線程都共用這個進程所包含的資源。

每一個線程可以對所屬進程的所有資源進行調度和運算,其中,線程可以是操作系統內核來控制調度,也可以是由用戶程式來控制調度。

基本定義

現代電腦,從組成部分上來看,大體可以分為硬體和軟體兩個部分。硬體是基礎,而軟體是運行在硬體之上的程式。

其中,軟體可以分為操作系統和應用程式:

  • 操作系統(Operation System):專註於對硬體的支持和交互管理並提供一個運行環境給應用程式使用
  • 應用程式(Application Program):能實現若幹功能且運行在操作系統中的軟體

由於線程可以由操作系統內核和用戶程式來控制調度,因此按照操作系統和應用程式兩個層次來分類。

線程可以主要分為內核線程和 用戶線程(應用線程)兩類,其中:

  • 內核線程(Kernel Thread):由操作系統內核支持和管理的線程,內核線程的創建,啟動,同步,銷毀,切換等均由操作系統完成。
  • 用戶(應用線程,Applciation Thread)線程(User Thread) :用戶(應用)線程的管理工作在用戶(應用)空間完成,它完全建立在用戶(應用)空間的線程庫上,由內核支持但不由內核管理,內核也無法感知用戶線程的存在。用戶(應用)線程的創建,啟動,同步,銷毀,切換等均在在用戶(應用)空間完成,不用切換到內核。

從Java領域來看,Java語言編譯後的位元組碼(Byte Code) 運行在JVM (Java 虛擬機)上,其中JVM其實是一個進程,所以Java屬於應用程式層。

我們都知道,Java的線程類為:java.lang.Thread,當任務不能在當前線程中執行時,我們就會去創建一個Thread對象。

我們在Java層通過new 關鍵字創建一個Thread對象,然後調用start()方法啟動該線程,那麼從線程的角度來看,主要可以分為:

  • Java應用程式層線程(Java Application Thread ):主要是Java語言編程的程式創建的Thread線程對象,屬於用戶空間
  • Java虛擬機層線程(Java JVM Thread ):主要是Java虛擬機中包含且支持和管理的線程,屬於用戶空間,
  • 操作系統層線程(OS Thread):根據操作系統的實際情況而定的抽象表示,主要是看操作系統和庫是否支持和管理的線程,一般Linux主要通過pthread庫來實現,早期版本不支持。

其中,在Hotspot JVM 中的 Java 線程與原生操作系統線程有直接的映射關係。當線程本地存儲、緩衝區分配、同步對象、棧、程式計數器等準備好以後,就會創建一個操作系統原生線程。

Java 線程結束,原生線程隨之被回收。操作系統負責調度所有線程,並把它們分配到任何可用的 CPU 上。

當原生線程初始化完畢,就會調用 Java 線程的 run() 方法。當線程結束時,會釋放原生線程和 Java 線程的所有資源。

一般在Hotspot JVM 後臺運行的系統線程主要有下麵幾方面:

  • 虛擬機線程(VM thread):這個線程等待 JVM 到達安全點操作出現。這些操作必須要在獨立的線程里執行,因為當堆修改無法進行時,線程都需要 JVM 位於安全點。這些操作的類型有:stop-theworld
  • 垃圾回收、線程棧 dump、線程暫停、線程偏向鎖(biased locking)解除。
  • 周期性任務線程: 這線程負責定時器事件(也就是中斷),用來調度周期性操作的執行。
  • GC 線程: 這些線程支持 JVM 中不同的垃圾回收活動。
  • 編譯器線程: 這些線程在運行時將位元組碼動態編譯成本地平臺相關的機器碼。
  • 信號分發線程: 這個線程接收發送到 JVM 的信號並調用適當的 JVM 方法處理。

由此可見,Java層到內層層的線程創建的大致流程:java.lang.Thread(Java應用程式層)—>Java Thread(JVM 層)->OS Thread(操作系統層)->pthread(根據操作系統的情況而定)->內核線程(Kernel Thread)。

基本模型

由於Java 中,JVM主要是由C/C++實現,所以Java層線程最終還是會映射到JVM層線程,而Java層的線程到操作系統層線程就得需要看具體的JVM的具體實現來決定。

一般來說,我們都把用戶線程看作更高層面的線程,而內核線程則向用戶線程提供支持。

由此可見,用戶線程和內核線程之間必然存在一定的映射關係,不同的操作系統可能採取不同的映射方式。

一般來說,按照映射方式來看,主要可以分為:多對一映射(用戶級方式),一對一映射(內核級方式) 和多對多映射(組合方式)3種方式。其中:

1. 多對一映射(用戶級方式)

多對一映射是指多個用戶線程被映射到一個內核線程上。每一個進程都對應著一個內核線程,進程內的所有線程也都對應著該內核線程。

多對一映射模型是指多條用戶線程映射同一條內核線程的情況,其中用戶線程由庫調度器進行調度,而內核線程由操作系統調度器來完成。

對於用戶線程而言,其會按照一定的策略輪流執行,具體的調度演算法有庫調度器完成。

任意一個時刻每一個進程中都只有一個用戶線程被執行,它們的執行都由用戶態的代碼完成切換。

在不支持線程的操作系統中有庫來實現線程式控制制,用戶線程創建,銷毀,切換的開銷代價比內核線程小。

因此,這種模式特點主要有兩點:

  • 首先,可以節省內核態到用戶態切換的開銷
  • 其次,線程的數量不會受到內核線程的限制

但是,因為線程切換的工作是由用戶態的代碼完成的,所以一個進程內,如果當一條線程發生阻塞時,與該內核線程對應的進程內的其他所有的用戶線程也會一起陷入阻塞。

2. 一對一映射(內核級方式)

一對一映射是指每個用戶線程都會被影射到一個內核線程上,用戶的整個生命周期都綁定到所映射的內核線程上。一個進程內可以有一個用戶線程和至少一個用戶線程,都對應著各自一個和至少一個內核線程,進程內的所有線程也都一一對應著各自內核線程。

一對一映射模型是指一條用戶線程對應著內核中的一條線程的情況,其中用戶線程由庫調度器進行調度,而內核線程由操作系統調度器來完成,而Java中採用的就是這種模型。

在這種方式下,多個CPU能並行執行同一個進程內的多個線程。

如果進程內的某個線程被阻塞,就可以切換到該進程的其他線程繼續執行,並且能切換執行其他進程的線程。

一對一映射模型是真正意義上的並行執行,因為這種模型下,創建一條Java的Thread線程是真正的在內核中創建並映射了一條內核線程的,執行過程中,一條線程不會因為另外一條線程的原因而發生阻塞等情況。

不過因為是每一個用線程都需要對應一個內核線程,這種直接映射內核線程的模式,所以數量會存在上限。

並且同一個核心中,多條線程的執行需要頻繁的發生上下文切換以及內核態與用戶態之間的切換,所以如果線程數量過多,切換過於頻繁會導致線程執行效率下降。

3. 多對多映射(組合方式)

多對多映射是指將一對一映射(內核級方式)和多對一映射(用戶級方式)組合起來,通過綜合兩者優點來形成的一種映射方式。該方式在用戶空間創建,銷毀,切換,調度線程,但是進程中的多個用戶線程會被影射到若幹個內核線程上。

多對多映射模型就可以避免上面一對一映射模型和多對一映射模型帶來的弊端,也就是多條用戶線程映射多條內核線程,這樣即可以避免一對一映射模型的切換效率問題和數量限制問題,也可以避免多對一映射模型的阻塞問題。

每一個內核線程負責與之綁定的若幹用戶線程,進程中的某個線程發生系統阻塞並不會導致整個進程阻塞,而阻塞該內核線程內的所對應的若幹用戶線程,其他線程依舊可以照常執行。

同時,因為用戶線程數量比內核線程數量多,所以能有效減少內核線程開銷。

基本實現

在java中,Java官方提供了三種方式來幫助我們實現一個線程,其中:

  • 第一種方式:繼承 Thread 對象:extends Thread
// 自定義線程對象
class ApplicationThread extends Thread { 
    public void run() { 
    // 線程需要執行的代碼 
    ...... 
    }
}

其中,Thread 類本質上是實現了Runnable 介面的一個實例,代表一個線程的實例。啟動線程的唯一方
法就是通過Thread 類的start()實例方法。start()方法是一個native 方法,它將啟動一個新線
程,並執行run()方法。

  • 第二種方式:實現 Runnable 介面(無返回值):implements Runnable
// 實現Runnable介面
class ApplicationThread implements Runnable {
    @Override 
    public void run() { 
    // 線程需要執行的代碼 
    ......
    }
}

其中,如果自己的類已經extends 另一個類,就無法直接extends Thread,此時,可以實現一個Runnable 介面。

  • 第三種方式:實現Callable 介面(有返回值):implements Callable
// 實現Runnable介面
class ApplicationThread implements Callable {
    @Override 
    public void run() { 
    // 線程需要執行的代碼 
    ......
    }
}

其中,執行Callable 任務後,可以獲取一個Future 的對象,在該對象上調用get 就可以獲取到Callable 任務返回的Object對象。

  • 第四種方式:基於線程池方式創建:線程和資料庫連接這些資源都是非常寶貴的資源。那麼每次需要的時候創建,不需要的時候銷
    毀,是非常浪費資源的。那麼我們就可以使用緩存的策略,也就是使用線程池。

Java 裡面線程池的頂級介面是Executor,但是嚴格意義上講Executor 並不是一個線程池,而只是一個執行線程的工具。真正的線程池介面是ExecutorService。

Java主要提供了newCachedThreadPool,newFixedThreadPool,newScheduledThreadPool以及newSingleThreadExecutor 等4種線程池。

目前業界線程池的設計,普遍採用的都是生產者 - 消費者模式。線程池的使用方是生產者,線程池本身是消費者。

Java 併發包里提供的線程池,比較強大且複雜。Java 提供的線程池相關的工具類中,最核心的是 ThreadPoolExecutor,通過名字你也能看出來,它強調的是 Executor,而不是一般意義上的池化資源。

ThreadPoolExecutor(
  int corePoolSize,
  int maximumPoolSize,
  long keepAliveTime,
  TimeUnit unit,
  BlockingQueue<Runnable> workQueue,
  ThreadFactory threadFactory,
  RejectedExecutionHandler handler) 

對於這些參數的意義,我們可以把線程池類比為一個項目組,而線程就是項目組的成員。其中:

  • corePoolSize:表示線程池保有的最小線程數。
  • maximumPoolSize:表示線程池創建的最大線程數。
  • keepAliveTime & unit:一個線程如果在一段時間內,都沒有執行任務,說明很閑,keepAliveTime 和 unit 就是用來定義這個“一段時間”的參數。也就是說,如果一個線程空閑了keepAliveTime & unit這麼久,而且線程池的線程數大於 corePoolSize ,那麼這個空閑的線程就要被回收。
  • workQueue:工作隊列。
  • threadFactory:通過這個參數你可以自定義如何創建線程名稱。
  • handler:通過這個參數你可以自定義任務的拒絕策略。

其中,Java在ThreadPoolExecutor 已經提供了以下 4 種策略:

  • CallerRunsPolicy:提交任務的線程自己去執行該任務
  • AbortPolicy:預設的拒絕策略,會 throws RejectedExecutionException
  • DiscardPolicy:直接丟棄任務,沒有任何異常拋出
  • DiscardOldestPolicy:丟棄最老的任務,其實就是把最早進入工作隊列的任務丟棄,然後把新任務加入到工作隊列

同時, Java 在 1.6 版本還增加了 allowCoreThreadTimeOut(boolean value) 方法,表示可以讓所有線程都支持超時。

調度方式

由於CPU的計算頻率非常高,每秒計算數十億次,因此可以將CPU的時間從毫秒的維度進行分段,每一小段叫作一個CPU時間片。

目前操作系統中主流的線程調度方式是:基於CPU時間片方式進行線程調度。

線程只有得到CPU時間片才能執行指令,處於執行狀態,沒有得到時間片的線程處於就緒狀態,等待系統分配下一個CPU時間片。

由於時間片非常短,在各個線程之間快速地切換,因此表現出來的特征是很多個線程在“同時執行”或者“併發執行”。

在Javs多視程環境中,為了保證所有線程都能按照一定的策略執行,JVM 需要有一個線程調變器支持工作。

這個調度器定義了線程測度的策略,通過特定的機製為多個線分配CPU的使用權,線程調度器中一般包含多種調度策略演算法,由這些演算法來決定CPU的分配。

除此之外,每個線程還有自己的優先順序(比如有高,中、低級別)調度演算法會通過這些優先順序來實現優先機制。

常見線程的調度模型目前主要分為兩種:(分時)協同式調度模型和搶占式調度模型。

  • 搶占式調度:
    • 系統按照線程優先順序分配CPU時間片
    • 優先順序高的線程優先分配CPU時間片,如果所有就緒線程的優先順序相同,那麼會隨機選擇一個,優先順序高的線程獲取的CPU時間片相對多一些。
    • 每個或程的執行時間和或候的切換高由調度落控劃,調度器按照某種略為每個線穆分配執行時間,
    • 調度器可能會為每個線整樣分配相的執行時間,也可能為某些特定線程分配較長的執行時間,甚至在極準情況下還可能不給某熱線程分!執行時同片,從而導致某技線相得不到執行,
    • 在搶占式調支機制下,一個線程的堵事不會導致整個進程堵客
  • (分時)協同式調度:
    • 系統平均分配CPU的時間片,所有線程輪流占用CPU,即在時間片調度的分配上所有線程“人人平等”。
    • 某一線相執行完後會主動通知調度器切換現下一個線程上繼續執行。
    • 在這種模式下,線程的執行時間由線程本身控物,也就是說線程的切換點是可以預先知道的。
    • 在這種模式下,如果某個錢程的邏輯輯存在問題,則可能導致系統運行到一半就阻塞了,最終會導致整個進程阻塞,甚至更糟可能導致整個系統崩潰。

由於目前大部分操作系統都是使用搶占式調度模型進行線程調度,Java的線程管理和調度是委托給操作系統完成的,與之相對應,Java的線程調度也是使用搶占式調度模型,因此Java的線程都有優先順序。

主要是 因為Java的線程調度涉及JVM的實現,JVM規範中規定每個線程都有各自的優先順序,且優先順序越高,則越優先執行。

但是,優先順序越高並不代表能獨占執行時間,可能優先順序越高得到的執行時間越長,反之,優先順序越低的線程得到執行時間越短,但不會出現不分配執行時間的情況。

假如有若幹個線程,我們想讓一些線程擁有更多的執行時間或者少分配點執行時間,那麼就可以通過設置線程的優先順序來實現。

所有處於可執行狀態的線程都在一個隊列中,且每個線程都有自己的優先順序,JVM 線程調度器會根據優先順序來決定每次的執行時間和執行頻率。

但是,優先順序高的線程一定會先執行嗎?我們能否在 Java 程式中通過優先順序值的大小來控制線程的執行順序呢?

答案是肯定不能的。主要是因為影響線程優先順序語義的因素有很多,具體如下:

  • 不同版本的操作系統和 JVM 都可能會產生不同的行為
  • 優先順序對於不同的操作系統調度器來說可能有不同的語義;有些操作系統的調度器不支持優先順序
  • 對於操作系統來說,線程的優先順序存在“全局”和“本地”之分,不同進程的優先順序一般相互獨立
  • 不同的操作系統對優先順序定義的值不一樣,Java 只定義了 1~10
  • 操作系統常常會對長時間得不到運行的線程給予增加一定的優先順序
  • 操作系統的線程調度器可能會線上程發生等待時有一定的臨時優先順序調整策略

JVM 線程調度器的調度策略決定了上層多線程的運行機制,每個線程執行的時間都由它分配管理。

調度器將按照線程優先順序對線程的執行時間進行分配,優先順序越高得到的 CPU執行時間越長,執行頻率也可能更大。

Java把線程優先順序分為10個級別,線程在創建時如果沒有明確聲明優先順序,則使用預設優先順序。

Java定義了 Thread.MIN_PRIORITY、Thread.NORM PRIORITY和 Thread.MAXPRIORITY這3個常量,分別代表最小優先順序值(1)、預設優先順序值(5)和最大優先順序值(10)。

此外,由於JVM 的實現是以宿主操作系統為基礎的,所以Java各優先順序與不同操作系統的原生線程優先順序必然存在著某種映射關係,這樣才能夠封裝所有操作系統的優先順序來提供統一的優先順序語義。

一般情況下,在Linux中可能要與-20~19之間的優先順序值進行映射,而Windows系統則有9個優先順序要映射。

生命周期

在 Java 領域,實現併發程式的主要手段就是多線程。線程是本身就是操作系統里的一個概念,不同的開發語言如 Java、C# 等都對其進行了封裝,但是萬變不離操作系統。

Java 語言里的線程本質上就是操作系統的線程,它們是一一對應的。

在操作系統層面,線程也有“生老病死”,專業的說法叫有生命周期。對於有生命周期的事物,要學好它,思路非常簡單,只要能搞懂生命周期中各個節點的狀態轉換機制即可。

雖然不同的開發語言對於操作系統線程進行了不同的封裝,但是對於線程的生命周期這部分,基本上是雷同的。

通用的線程生命周期基本上可以用 初始狀態、可運行狀態、運行狀態、休眠狀態和終止狀態等“五態模型”來描述。

Java 語言中線程共有六種狀態,分別是:NEW(初始化狀態)RUNNABLE(可運行 / 運行狀態)BLOCKED(阻塞狀態)WAITING(無時限等待)TIMED_WAITING(有時限等待)TERMINATED(終止狀態)。

其實在操作系統層面,Java 線程中的 BLOCKED、WAITING、TIMED_WAITING 是一種狀態,即前面我們提到的休眠狀態。也就是說只要 Java 線程處於這三種狀態之一,那麼這個線程就永遠沒有 CPU 的使用權。

其中,BLOCKED、WAITING、TIMED_WAITING 可以理解為線程導致休眠狀態的三種原因。那具體是哪些情形會導致線程從 RUNNABLE 狀態轉換到這三種狀態呢?而這三種狀態又是何時轉換回 RUNNABLE 的呢?以及 NEW、TERMINATED 和 RUNNABLE 狀態是如何轉換的?

1. RUNNABLE 與 BLOCKED 的狀態轉換

只有一種場景會觸發這種轉換,就是線程等待 synchronized 的隱式鎖。synchronized 修飾的方法、代碼塊同一時刻只允許一個線程執行,其他線程只能等待,這種情況下,等待的線程就會從 RUNNABLE 轉換到 BLOCKED 狀態。而當等待的線程獲得 synchronized 隱式鎖時,就又會從 BLOCKED 轉換到 RUNNABLE 狀態。

2. RUNNABLE 與 WAITING 的狀態轉換

總體來說,有三種場景會觸發這種轉換,其中:

  • 第一種場景,獲得 synchronized 隱式鎖的線程,調用無參數的 Object.wait() 方法。其中,wait() 方法我們在上一篇講解管程的時候已經深入介紹過了,這裡就不再贅述。
  • 第二種場景,調用無參數的 Thread.join() 方法。其中的 join() 是一種線程同步方法,例如有一個線程對象 thread A,當調用 A.join() 的時候,執行這條語句的線程會等待 thread A 執行完,而等待中的這個線程,其狀態會從 RUNNABLE 轉換到 WAITING。當線程 thread A 執行完,原來等待它的線程又會從 WAITING 狀態轉換到 RUNNABLE。
  • 第三種場景,調用 LockSupport.park() 方法。其中的 LockSupport 對象,也許你有點陌生,其實 Java 併發包中的鎖,都是基於它實現的。調用 LockSupport.park() 方法,當前線程會阻塞,線程的狀態會從 RUNNABLE 轉換到 WAITING。調用 LockSupport.unpark(Thread thread) 可喚醒目標線程,目標線程的狀態又會從 WAITING 狀態轉換到 RUNNABLE。
3. RUNNABLE 與 TIMED_WAITING 的狀態轉換

有五種場景會觸發這種轉換,其中:

  • 調用帶超時參數的 Thread.sleep(long millis) 方法。
  • 獲得 synchronized 隱式鎖的線程,調用帶超時參數的 Object.wait(long timeout) 方法。
  • 調用帶超時參數的 Thread.join(long millis) 方法。
  • 調用帶超時參數的 LockSupport.parkNanos(Object blocker, long deadline) 方法。
  • 調用帶超時參數的 LockSupport.parkUntil(long deadline) 方法。
4. 從 NEW 到 RUNNABLE 的狀態

Java 剛創建出來的 Thread 對象就是 NEW 狀態,而創建 Thread 對象主要有兩種方法:

  • 首先,第一種方式是繼承 Thread 對象,重寫 run() 方法
// 自定義線程對象
class ApplicationThread extends Thread { 
    public void run() { 
    // 線程需要執行的代碼 
    ...... 
    }
}
// 創建線程對象
ApplicationThread applicationThread = new ApplicationThread();
  • 其次,另一種方式是實現 Runnable 介面,重寫 run() 方法,並將該實現類作為創建 Thread 對象的參數
// 實現Runnable介面
class ApplicationThread implements Runnable {
    @Override 
    public void run() { 
    // 線程需要執行的代碼 
    ......
    }
}
// 創建線程對象
Thread thread = new Thread(new ApplicationThread());

NEW 狀態的線程,不會被操作系統調度,因此不會執行。Java 線程要執行,就必須轉換到 RUNNABLE 狀態。從 NEW 狀態轉換到 RUNNABLE 狀態很簡單,只要調用線程對象的 start() 方法即可。

5. 從 RUNNABLE 到 TERMINATED

線程執行完 run() 方法後,會自動轉換到 TERMINATED 狀態,當然如果執行 run() 方法的時候異常拋出,也會導致線程終止。有時候我們需要強制中斷 run() 方法的執行。

一般來說, run() 方法訪問一個很慢的網路,我們等不下去了,想終止怎麼辦呢?

Java 的 Thread 類裡面倒是有個 stop() 方法,不過已經標記為 @Deprecated,所以不建議使用了。正確的姿勢其實是調用 interrupt() 方法。

那麼,stop() 和 interrupt() 方法的主要區別是什麼呢?

  • stop() 方法會真的殺死線程,不給線程喘息的機會,如果線程持有 ReentrantLock 鎖,被 stop() 的線程並不會自動調用 ReentrantLock 的 unlock() 去釋放鎖,那其他線程就再也沒機會獲得 ReentrantLock 鎖,這實在是太危險了。所以該方法就不建議使用了,類似的方法還有 suspend() 和 resume() 方法,這兩個方法同樣也都不建議使用。

  • interrupt() 方法僅僅是通知線程,線程有機會執行一些後續操作,同時也可以無視這個通知。

被 interrupt 的線程,是怎麼收到通知的呢?

  • 一種是異常:
  1. 線程 A 處於 WAITING、TIMED_WAITING 狀態時,如果其他線程調用線程 A 的 interrupt() 方法,會使線程 A 返回到 RUNNABLE 狀態,同時線程 A 的代碼會觸發 InterruptedException 異常。上面我們提到轉換到 WAITING、TIMED_WAITING 狀態的觸發條件,都是調用了類似 wait()、join()、sleep() 這樣的方法,我們看這些方法的簽名,發現都會 throws InterruptedException 這個異常。這個異常的觸發條件就是:其他線程調用了該線程的 interrupt() 方法。
  2. 當線程 A 處於 RUNNABLE 狀態時,並且阻塞在 java.nio.channels.InterruptibleChannel 上時,如果其他線程調用線程 A 的 interrupt() 方法,線程 A 會觸發 java.nio.channels.ClosedByInterruptException 這個異常;而阻塞在 java.nio.channels.Selector 上時,如果其他線程調用線程 A 的 interrupt() 方法,線程 A 的 java.nio.channels.Selector 會立即返回。
  • 另一種是主動檢測:
  1. 如果線程處於 RUNNABLE 狀態,並且沒有阻塞在某個 I/O 操作上,例如中斷計算圓周率的線程 A,這時就得依賴線程 A 主動檢測中斷狀態了。
  2. 如果其他線程調用線程 A 的 interrupt() 方法,那麼線程 A 可以通過 isInterrupted() 方法,檢測是不是自己被中斷。

寫在最後

首先,管程(Monitor)就是一對monitorenter和monitorexit指令組成的一個對象監視器。任何線程想要訪問該資源,就要排隊進入監控範圍。進入之後,接受檢查,不符合條件,則要繼續等待,直到被通知,然後繼續進入監視器。

在Java中,每個加鎖的對象都綁定著一個管程(監視器)。首先,線程訪問加鎖對象,就是去擁有一個監視器的過程,所有線程訪問共用資源,都需要先擁有監視器。其次,監視器至少有兩個等待隊列:一個是進入監視器的等待隊列,一個是條件變數對應的等待隊列。最後,當監視器要求的條件滿足後,位於條件變數下等待的線程需要重新排隊,等待通知再進入監視器。

其次,線程(Thread)是進程(Process)中的運行單位,可以把線程看作輕量級的進程。

線程按照操作系統和應用程式兩個層次來分類,主要分為 內核線程(Kernel Thread)和用戶(應用線程,Applciation Thread)線程(User Thread) 。

在Java領域中,線程可以分為:Java應用程式層線程(Java Application Thread ),Java虛擬機層線程(Java JVM Thread )和操作系統層線程(OS Thread)。

其中,Java層到內層層的線程創建的大致流程:java.lang.Thread(Java應用程式層)—>Java Thread(JVM 層)->OS Thread(操作系統層)->pthread(根據操作系統的情況而定)->內核線程(Kernel Thread)。

另外,線程按照映射方式來看,主要可以分為:多對一映射(用戶級方式),一對一映射(內核級方式) 和多對多映射(組合方式)3種方式。

Java 語言中線程共有六種狀態,分別是:NEW(初始化狀態)RUNNABLE(可運行 / 運行狀態)BLOCKED(阻塞狀態)WAITING(無時限等待)TIMED_WAITING(有時限等待)TERMINATED(終止狀態)。

Java中實現線程的方式:繼承 Thread 對象:extends Thread,實現 Runnable 介面(無返回值):implements Runnable ,實現Callable 介面(有返回值):implements Callable,基於線程池方式創建等。

常見線程的調度模型目前主要分為兩種:(分時)協同式調度模型和搶占式調度模型,Java的線程調度也是使用搶占式調度模型,因此Java的線程都有優先順序。

Java 線程的調度機制由 JVM 實現,Java定義了 Thread.MIN_PRIORITY、Thread.NORM PRIORITY和 Thread.MAXPRIORITY這3個常量,分別代表最小優先順序值(1)、預設優先順序值(5)和最大優先順序值(10)。

綜上所述,我想關於Java中的線程機制,看到這個地方,你一定樂然於胸,希望未來的我們更加優秀!

版權聲明:本文為博主原創文章,遵循相關版權協議,如若轉載或者分享請附上原文出處鏈接和鏈接來源。


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

-Advertisement-
Play Games
更多相關文章
  • 一、Mysql的系統架構圖 二、Mysql存儲引擎 Mysql中的數據是通過一定的方式存儲在文件或者記憶體中的,任何方式都有不同的存儲、查找和更新機制,這意味著選擇不同的方式對於數據的存取有效率的差距。 這種不同的存儲方式在 MySQL中被稱作存儲引擎。 存儲引擎是Mysql資料庫系統的底層組件,數據 ...
  • 如今短視頻已成為人們娛樂社交的主要形式,很多用戶也開始由觀眾逐漸轉變為短視頻製作傳播者,然而複雜的視頻剪輯工具卻令他們望而止步。如何才能降低短視頻製作剪輯門檻,讓更多無經驗者也能製作出優質的短視頻內容,並樂於分享生活趣事呢? 華為HMS Core視頻編輯服務6.6.0版本近期上線AI精彩片段能力,能 ...
  • Date日期 日期對象的定義(使用new關鍵詞) 1.獲取當前的時間(本地的時間) var date = new Date() //不傳參就是獲取當前時間 2.獲取指定的時間 var date = new Date(123456) //一個參數毫秒值 將這個毫秒值去加上對應的1970.1.1 0:0 ...
  • 今天在做css定位的時候遇到一個問題,我想用fixed定位下來,但是發現這個時候定義的百分百寬度不隨著父元素走了而是整個屏幕的百分百,這個就很尷尬了,也不能固定寬度吧,畢竟還要寬度自適應。 這個時候發現了一個position的屬性 sticky 它是relative和fixed的結合體可以理解為,當 ...
  • 最近,在對公司的一個老項目進行優化調整。有個使用的三方插件報表頁面,一旦查詢時間過長就會自動異常並使瀏覽器崩潰,由於這個插件只有個前人遺留的dll文件,實在看不懂裡面的代碼無從下手,既然項目前端大部分是基於EasyUI做的,想著就直接用EasyUI的DataGrid做數據報表明細展示。 由於之前很少 ...
  • 網頁是一個頁面,網站是由多個網頁組成的! 我們在使用代碼編寫的時候能看到這樣的東西,具體內容如下,比較基礎: 認識SEO? SEO就是搜索引擎優化,作用就是你找查找相關內容時,能夠優先給你展示這個內容。 SEO三大標簽(一般由想乾人員提供): title(網頁標簽)、description(網頁描述 ...
  • 項目背景 我們的系統(一個 ToB 的 Web 單頁應用)前端單頁應用經過多年的迭代,目前已經累積有大幾十萬行的業務代碼,30+ 路由模塊,整體的代碼量和複雜度還是比較高的。 項目整體是基於 Vue + TypeScirpt,而構建工具,由於最早項目是經由 vue-cli 初始化而來,所以自然而然使 ...
  • 什麼時候精靈圖呢? 通常在渲染頁面的時候,需要伺服器向我們發送數據,但有的時候一個頁面需要多張圖時,伺服器就會處於連續發圖的工作狀態,但如果我們把需要的圖都放在一張圖上,這樣可以大大的減少服務的工作負擔,打個比喻。伺服器發一張圖是,工作流程是:找到圖片——讀取圖片——發送圖片,如果是發送5個圖片時, ...
一周排行
    -Advertisement-
    Play Games
  • 使用原因: 在我們服務端調用第三方介面時,如:支付寶,微信支付,我們服務端需要模擬http請求並加上一些自己的邏輯響應給前端最終達到我們想要的效果 1.使用WebClient 引用命名空間 using System.Net; using System.Collections.Specialized; ...
  • WPF 實現帶蒙版的 MessageBox 消息提示框 WPF 實現帶蒙版的 MessageBox 消息提示框 作者:WPFDevelopersOrg 原文鏈接: https://github.com/WPFDevelopersOrg/WPFDevelopers.Minimal 框架使用大於等於.N ...
  • 一、JSON(JavaScript Object Notation)的簡介: ① JSON和XML類似,主要用於存儲和傳輸文本信息,但是和XML相比,JSON更小、更快、更易解析、更易編寫與閱讀。 ② C、Python、C++、Java、PHP、Go等編程語言都支持JSON。 二、JSON語法規則: ...
  • 1.避免Scoped模式註冊的服務變成Singleton模式 當提供一個生命周期模式為Singleton的服務實例時,如果發現該服務中還依賴生命周期模式為Scoped的服務實例(Scoped服務實例將被一個Singleton服務實例所引用),那麼這個被依賴的Scoped服務實例最終會成為一個Sing ...
  • 索引時資料庫提高數據查詢處理性能的一個非常關鍵的技術,索引的使用可以對性能產生上百倍甚至上千倍的影響。接下來,會介紹索引的基本原理、概念,並深入學習資料庫中所使用的索引結構和存儲方式,以及如何管理、維護索引等。 1.索引的基本概念 索引時用來快速查詢表記錄的一種存儲結構,一般使用索引有一下兩個方面: ...
  • django2 路由控制器 Route路由,是一種映射關係。路由是把客戶端請求的url路徑和用戶請求的應用程式,這裡意指django裡面的視圖進行綁定映射的一種關係。 請求路徑和視圖函數不是一一對應的關係 在django中所有的路由最終都被保存到一個叫urlpatterns的文件里,並且該文件必須在 ...
  • 1、我們的目標是獲取微博某博主的全部圖片、視頻 2、拿到網址後 我們先觀察 打開F12 隨著下滑我們發現載入出來了一個叫mymblog的東西,展開響應發現需要的東西就在裡面 3、重點來了!!! 通過觀察發現第二頁比第一頁多了參數since_id 而第二頁的since_id參數剛好在上一頁中能獲取到, ...
  • 一、實現原理 在Servlet3協議規範中,包含在JAR文件/META-INFO/resources/路徑下的資源可以直接訪問。 二、舉例說明 如下圖所示,是我新建的一個Spring Boot Starter項目:zimug-minitor-threadpool,用於實現可配置、可觀測的線程池。其中 ...
  • 精華筆記: static final常量:應用率高 必須聲明同時初始化 由類名打點來訪問,不能被改變 建議:常量所有字母都大寫,多個單詞用_分隔 編譯器在編譯時會將常量直接替換為具體的數,效率高 何時用:數據永遠不變,並且經常使用 抽象方法: 由abstract修飾 只有方法的定義,沒有具體的實現( ...
  • Python有一個for...else語法,它的寫法如下 for i in range(0,100): if i == 3: break else: print("Not found") 該語句表示:若for迴圈遍歷完畢,則執行else部分的語句。也就是說上述代碼不會有任何輸出,而下述代碼會輸出“N ...