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

寫在開頭

我們都知道,經過多年的發展和無數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 的執行緒,是怎么收到通知的呢?
- 一種是例外:
- 執行緒 A 處于 WAITING、TIMED_WAITING 狀態時,如果其他執行緒呼叫執行緒 A 的 interrupt() 方法,會使執行緒 A 回傳到 RUNNABLE 狀態,同時執行緒 A 的代碼會觸發 InterruptedException 例外,上面我們提到轉換到 WAITING、TIMED_WAITING 狀態的觸發條件,都是呼叫了類似 wait()、join()、sleep() 這樣的方法,我們看這些方法的簽名,發現都會 throws InterruptedException 這個例外,這個例外的觸發條件就是:其他執行緒呼叫了該執行緒的 interrupt() 方法,
- 當執行緒 A 處于 RUNNABLE 狀態時,并且阻塞在 java.nio.channels.InterruptibleChannel 上時,如果其他執行緒呼叫執行緒 A 的 interrupt() 方法,執行緒 A 會觸發 java.nio.channels.ClosedByInterruptException 這個例外;而阻塞在 java.nio.channels.Selector 上時,如果其他執行緒呼叫執行緒 A 的 interrupt() 方法,執行緒 A 的 java.nio.channels.Selector 會立即回傳,
- 另一種是主動檢測:
- 如果執行緒處于 RUNNABLE 狀態,并且沒有阻塞在某個 I/O 操作上,例如中斷計算圓周率的執行緒 A,這時就得依賴執行緒 A 主動檢測中斷狀態了,
- 如果其他執行緒呼叫執行緒 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中的執行緒機制,看到這個地方,你一定樂然于胸,希望未來的我們更加優秀!
著作權宣告:本文為博主原創文章,遵循相關著作權協議,如若轉載或者分享請附上原文出處鏈接和鏈接來源,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/500854.html
標籤:其他
上一篇:設計模式之建造者模式
下一篇:設計模式之代理模式
