1. 程式人生 > >十一、JVM(HotSpot)Java記憶體模型與執行緒

十一、JVM(HotSpot)Java記憶體模型與執行緒

注:本博文主要是基於JDK1.7會適當加入1.8內容。

1、Java記憶體模型

記憶體模型:在特定的操作協議下,對特定的記憶體或快取記憶體進行讀寫訪問的抽象過程。不同的物理機擁有不一樣的記憶體模型,而Java虛擬機器也擁有自己的記憶體模型。
主要目標:定義程式中各個變數的訪問規則,即在虛擬機器中將變數儲存到記憶體和從記憶體中取出變數這樣的底層細節。這裡的變數與Java程式設計中所說的變數存在區別,它包括了例項變數、靜態欄位和構成陣列物件的元素,但不包括區域性變數和方法引數,因為後者都是執行緒私有不存在共享也就不存在競爭問題。

(1)主記憶體和工作記憶體

Java記憶體模型規定了所有的變數儲存下主記憶體中,每個執行緒有自己的工作記憶體,工作記憶體中儲存了該執行緒使用到的主記憶體副本拷貝,執行緒對變數的所有操作需要在工作記憶體中進行,而不能直接讀寫主記憶體中的變數。Java工作記憶體和主記憶體

(2)記憶體間互動操作

  • lock,鎖定:作用於主記憶體的變數,把一個變數標識為一條執行緒獨佔的狀態;
  • unlock,解鎖:作用於主記憶體的變數,把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定;
  • read,讀取:作用於主記憶體的變數,把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中,方便隨後的load動作使用;
  • load,載入:作用於工作記憶體的變數,把read操作從主記憶體中得到的變數值放到工作記憶體的變數副本中;
  • use,使用:作用域工作記憶體的變數,把工作記憶體中的一個變數的值傳給執行引擎,每當虛擬機器遇到一個需要使用到變數的值得位元組碼指令時會執行這個操作;
  • assign,賦值:作用於工作記憶體的變數,把一個從執行引擎收到的值賦給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作;
  • store,儲存:作用於工作記憶體的變數,把工作記憶體中一個變數的值傳送給主記憶體中,方便隨後的write操作使用;
  • write,寫入:作用於主記憶體的變數,把store操作從工作記憶體中得到的變數的值放入主記憶體變數中。

從工作記憶體同步到主記憶體需要順序執行store和write操作,從主記憶體複製到工作記憶體需要順序執行read和load操作。Java記憶體模型規定在執行上必須滿足一下規則:

  • 不允許read和load、store和write操作單一出現,即不允許一個變數從主記憶體讀取了但工作記憶體不接收,或者工作記憶體發起回寫了但主記憶體不接收;
  • 不允許一個執行緒丟棄最近的assign操作,即變數在工作記憶體中改變了之後必須要把變化同步到主記憶體中;
  • 不允許一個執行緒無原因的把陣列從執行緒的工作記憶體同步回主記憶體(效能考量);
  • 一個新變數只能在主記憶體中誕生,不允許在工作記憶體中直接使用一個未初始化的變數;
  • 一個變數同一時刻只允許一條執行緒對其lock操作,但lock操作可以被同一條執行緒執行多次,多次執行lock後需要執行相同次數的unlock操作,變數才會解鎖;
  • 如果對一個變數進行lock操作,會清空工作記憶體中該變數的值,在執行引擎使用這個變數前需要重新執行load和assign操作;
  • 如果一個變數事先沒有被lock鎖定,那不允許unlock操作出現,也不允許unlock其他一個執行緒鎖定的執行緒;
  • 對一個變數執行unlock操作之前,必須先把該變數同步回主記憶體中。

2、對於volatile型變數的特殊規則

  • 可見性
  • 禁止指令重排序

volatile變數只能保證可見性,在不符合以下兩條規則的運算場景中,仍然需要通過加鎖保證原子性:(1)運算結果並不依賴變數的當前值,或者能夠確保只有單一的執行緒修改變數的值;(2)變數不需要與其他的狀態變數共同參與不變約束。

(1)原子性、可見性和有序性

原子性:由Java記憶體模型直接保證原子性變數操作包括read、load、assign、use、store和write。
可見性:volatile實現,final與synchronized也可以實現。
有序性:volatile實現,synchronized也可實現。

(2)先行發生原則

  • 程式次序規則:同一個執行緒中,按照程式程式碼順序。
  • 管程鎖定規則:一個unlock操作先行發生與後面對同一個鎖的lock操作。
  • volatile變數規則:volatile變數寫操作先行發生於後面這個變數的讀操作。
  • 執行緒啟動規則:Thread物件start方法先行發生於此執行緒的每個操作。
  • 執行緒終止規則:Thread物件所有操作先行發生於對此物件的終止檢測,通過Thread.join()方法結束,Thread.isAlive()返回值檢測。
  • 執行緒中斷規則:對執行緒interrupt()方法呼叫先行發生於被中斷的程式碼檢測到中斷事件的發生,通過Thread.interrupt()方法檢測。
  • 物件終結規則:一個物件初始化完成先行發生於它的finalize()方法的開始。
  • 傳遞性:如果A先行發生於B,B先行發生於C,那麼A先行發生於C。

時間先後順序發生與先行性發生原則之間基本沒有太大關係,衡量併發安全問題的時候不要受到時間順序的干擾,一切必須以先行發生原則為準。

(3)執行緒的實現:執行緒是CPU排程的基本單位

實現執行緒的3種方式分別是:使用核心執行緒實現;使用使用者執行緒實現;使用使用者執行緒加輕量級程序混合實現。

核心執行緒

直接由作業系統核心支援的執行緒,由核心完成執行緒切換,核心通過操作排程器對執行緒進行排程,並負責執行緒的任務對映到各個處理器上。每個核心執行緒可視為核心的一個分身,這樣作業系統就有能力同時處理多件時間,支援多執行緒的核心就叫做多執行緒核心。程式一般不會直接使用核心執行緒而是使用核心執行緒的一種高階介面——輕量級程序,也就是通常意義上所說的執行緒。輕量級程序具有它的侷限性:首先,由於是基於系統核心執行緒實現,所以執行緒操作如建立、析構、同步等都需要進行系統呼叫,而系統呼叫代價相對較高,需要在使用者態和核心態來回切換;其次,每個輕量級程序都需要一個核心執行緒支援,因此消耗一定的核心資源,核心資源是有限的,所以輕量級程序也是有限的。

使用者執行緒

廣義上一個執行緒主要不是核心執行緒就是使用者執行緒,這樣輕量級程序(基於核心執行緒的高階介面)也屬於使用者執行緒,但它始終建立在核心執行緒上,許多操作需要進行系統呼叫,效率受限。狹義上,使用者執行緒是完全建立在使用者控制元件的執行緒庫上,系統核心不能感知執行緒存在的實現。如果實現得當,不需要切換到核心態,操作快速低耗,也可支援規模更大的執行緒數量。

使用者執行緒和輕量級程序混合

使用者執行緒完全建立在使用者控制元件中,因此使用者執行緒建立、切換、析構等操作廉價並支援大規模使用者執行緒併發,作業系統提供支援輕量級程序作為使用者執行緒和核心執行緒的橋樑,核心提供執行緒排程功能及處理對映,並且使用者執行緒的系統呼叫需要通過輕量級程序來完成,大大降低整個程序被完全阻塞的風險。

Java執行緒

Windows和Linux版本都是一對一的執行緒模型實現,一條Java執行緒對映到一條輕量級程序中,因為Windows和Linux系統提供的執行緒模型是一對一的。Solaris平臺由於作業系統執行緒特性可以同時支援一對一、多對多的執行緒模型。

(4)Java執行緒排程:系統為執行緒分配處理器使用權的過程,分別是協同式執行緒排程和搶佔式執行緒排程

協同式執行緒排程:執行緒執行時間由執行緒本身控制,執行緒把自己工作完成後主動通知系統切換到另一個執行緒。優點:實現簡單,沒什麼執行緒同步問題,Lua語言中的協程就是這類實現;缺點:執行緒執行時間不可控,如果執行緒出現問題會一直阻塞。
搶佔式執行緒排程:每個執行緒將有系統分配執行時間,執行緒切換不由執行緒本身決定。優點:執行緒執行時間系統可控,不會出現一個執行緒導致整個程序阻塞的問題,Java使用的執行緒排程方式就是搶佔式執行緒排程。如果想自定義執行緒執行時間或者優先順序,Java定義了Thread.MIN_PRIORITY和Thread.MAX_PRIORITY,但是不能完全依賴,因為Java執行緒要對映到作業系統實現,不同的作業系統對執行緒優先順序支援不一樣,優先順序級別定義也不一定一樣。

(5)狀態轉換:新建–執行–等待(無限期和有限期)–阻塞–結束