1. 程式人生 > >記憶體模型與多執行緒設計-JMM模型

記憶體模型與多執行緒設計-JMM模型

RoadMap

1 JMM模型

    JMM 全稱,Java Memory Model. 這個記憶體模型與Stack,heap GC分代的記憶體模型,不是一回事,兩者是通過不通的維度,將硬體訪問抽象出來的一層抽象的邏輯模型,JVM遮蔽了硬體的直接操作。 GC分代的記憶體模型更加貼近與垃圾回收和記憶體分配使用的理解,而JMM模型更加貼近,多執行緒和記憶體之間的通訊

1.1 工作記憶體和主記憶體

    Java記憶體模型規定了所有的變數都儲存在主記憶體(Main Memory)中。每條執行緒還有自己的工作記憶體,執行緒的工作記憶體中儲存了被該執行緒使用到的變數的主記憶體副本拷貝,執行緒對變數的所有操作(讀取、賦值等)都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數。不同的執行緒之間也無法直接訪問對方工作記憶體中的變數,執行緒間變數值的傳遞均需要通過主記憶體來完成。

需要注意的是, 放在住記憶體的變數包括,例項欄位、靜態欄位和構成陣列物件的元素,但不包括區域性變數與方法引數,因為後者是執行緒獨有的。

 

1.2 記憶體間的互動操作

  執行緒間如果要完成變成的同步和共享,必須經歷下面2個步驟。

   1. 執行緒A必須要把執行緒A的工作記憶體更新過的變數重新整理到主記憶體去。

   2. 執行緒B到主記憶體中去讀取執行緒A更新過的共享變數

這些通訊操作是被JMM遮蔽的,要保證變數的執行緒安全共享 需要使用Java的同步塊(synchonrized),或者其他併發工具。這裡強調的是安全共享,在不加同步塊,和併發工具的情況下,變數也是可以被共享的,只是不能保證讀都最新資料,就是常說的 髒讀,錯讀等

 2  原子操作與指令重排序

2.1 原子操作

    根據工作記憶體與住記憶體的互動協議,一個 變數從主記憶體拷貝到工作記憶體,與工作記憶體同步到主記憶體,會經過一下八個原子操作。

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

JMM規定如果要把一個變數從主記憶體複製到工作記憶體,那就要順序地執行read和load操作,如果要把變數從工作記憶體同步回主記憶體,就要順序地執行store和write操作。

    注意,Java記憶體模型只要求上述兩個操作必須按順序執行,而沒有保證是連續執行。也就是說,read與load之間、store與write之間是可插入其他指令的,如對主記憶體中的變數a、b進行訪問時,一種可能出現順序是read a、read b、load b、load a。    

除了以上的順序約束以外,還規定了其他的約束:

    a.  不允許read和load、store和write操作之一單獨出現,即不允許一個變數從主記憶體讀取了但工作記憶體不接受,或者從工作記憶體發起回寫了但主記憶體不接受的情況出現。

    b. 不允許一個執行緒丟棄它的最近的assign操作,即變數在工作記憶體中改變了之後必須把該變化同步回主記憶體。

    c. 不允許一個執行緒無原因地(沒有發生過任何assign操作)把資料從執行緒的工作記憶體同步回主記憶體中。

    d.  一個新的變數只能在主記憶體中“誕生”,不允許在工作記憶體中直接使用一個未被初始化(load或assign)的變數,換句話說,就是對一個變數實施use、store操作之前,必須先執行過了assign和load操作。

    e.  一個變數在同一個時刻只允許一條執行緒對其進行lock操作,但lock操作可以被同一條執行緒重複執行多次,多次執行lock後,只有執行相同次數的unlock操作,變數才會被解鎖。

    f. 如果對一個變數執行lock操作,那將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前,需要重新執行load或assign操作初始化變數的值。

    g. 如果一個變數事先沒有被lock操作鎖定,那就不允許對它執行unlock操作,也不允許去unlock一個被其他執行緒鎖定住的變數。 對一個變數執行unlock操作之前,必須先把此變數同步回主記憶體中(執行store、write操作)。

2.2 Volatile

    關鍵字volatile可以說是Java虛擬機器提供的最輕量級的同步機制. 當一個變數定義為volatile之後,它將具備兩種特性,第一是保證此變數對所有執行緒的可見性. 第二個語義是禁止指令重排序優化.

    這裡的“可見性”是指當一條執行緒修改了這個變數的值,新值對於其他執行緒來說是可以立即得知的。而普通變數不能做到這一點,普通變數的值線上程間傳遞均需要通過主記憶體來完成,例如,執行緒A修改一個普通變數的值。需要注意的是,volatile只是保證了可見性,很容易誤解為volatile變數在各個執行緒中是一致的,所以基於volatile變數的運算在併發下是安全的。但是Java裡面的運算並非原子操作,各個工作區的volaitile的變數可能存在不一致的情況,導致volatile變數的運算在併發下一樣是不安全的。

3 三大特性

Java記憶體模型是圍繞著在併發過程中如何處理原子性、可見性和有序性這3個特徵來建立的

3.1 原子性

    原子性(Atomicity):由Java記憶體模型來直接保證的原子性變數操作包括read、load、assign、use、store和write,我們大致可以認為基本資料型別的訪問讀寫是具備原子性的(例外就是long和double的非原子性協定,讀者只要知道這件事情就可以了,無須太過在意這些幾乎不會發生的例外情況)。 如果應用場景需要一個更大範圍的原子性保證(經常會遇到),Java記憶體模型還提供了lock和unlock操作來滿足這種需求,儘管虛擬機器未把lock和unlock操作直接開放給使用者使用,但是卻提供了更高層次的位元組碼指令monitorenter和monitorexit來隱式地使用這兩個操作,這兩個位元組碼指令反映到Java程式碼中就是同步塊——synchronized關鍵字,因此在synchronized塊之間的操作也具備原子性。

3.2 可見性

    可見性(Visibility):可見性是指當一個執行緒修改了共享變數的值,其他執行緒能夠立即得知這個修改。Java記憶體模型是通過在變數修改後將新值同步回主記憶體,在變數讀取前從主記憶體重新整理變數值這種依賴主記憶體作為傳遞媒介的方式來實現可見性的,無論是普通變數還是volatile變數都是如此,普通變數與volatile變數的區別是,volatile的特殊規則保證了新值能立即同步到主記憶體,以及每次使用前立即從主記憶體重新整理。因此,可以說volatile保證了多執行緒操作時變數的可見性,而普通變數則不能保證這一點。

    除了volatile之外,Java還有兩個關鍵字能實現可見性,即synchronized和final。同步塊的可見性是由“對一個變數執行unlock操作之前,必須先把此變數同步回主記憶體中(執行store、write操作)”這條規則獲得的,而final關鍵字的可見性是指:被final修飾的欄位在構造器中一旦初始化完成,並且構造器沒有把“this”的引用傳遞出去(this引用逃逸是一件很危險的事情,其他執行緒有可能通過這個引用訪問到“初始化了一半”的物件),那在其他執行緒中就能看見final欄位的值。

3.3 有序性

    有序性(Ordering):Java記憶體模型的有序性在前面講解volatile時也詳細地討論過了,Java程式中天然的有序性可以總結為一句話:如果在本執行緒內觀察,所有的操作都是有序的;如果在一個執行緒中觀察另一個執行緒,所有的操作都是無序的。前半句是指“執行緒內表現為序列的語義”(Within-Thread As-If-Serial Semantics),後半句是指“指令重排序”現象和“工作記憶體與主記憶體同步延遲”現象。 Java語言提供了volatile和synchronized兩個關鍵字來保證執行緒之間操作的有序性,volatile關鍵字本身就包含了禁止指令重排序的語義,而synchronized則是由“一個變數在同一個時刻只允許一條執行緒對其進行lock操作”這條規則獲得的,這條規則決定了持有同一個鎖的兩個同步塊只能序列地進入。

4 先行先發原則

    如果Java記憶體模型中所有的有序性都僅僅靠volatile和synchronized來完成,那麼有一些操作將會變得很煩瑣,但是我們在編寫Java併發程式碼的時候並沒有感覺到這一點,這是因為Java語言中有一個“先行發生”(happens-before)的原則。這個原則非常重要,它是判斷資料是否存在競爭、執行緒是否安全的主要依據,依靠這個原則,我們可以通過幾條規則一攬子地解決併發環境下兩個操作之間是否可能存在衝突的所有問題。

  • 程式次序規則:一個執行緒內,按照程式碼順序,書寫在前面的操作先行發生於書寫在後面的操作
  • 鎖定規則:一個unLock操作先行發生於後面對同一個鎖額lock操作
  • volatile變數規則:對一個變數的寫操作先行發生於後面對這個變數的讀操作
  • 傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C
  • 執行緒啟動規則:Thread物件的start()方法先行發生於此執行緒的每個一個動作
  • 執行緒中斷規則:對執行緒interrupt()方法的呼叫先行發生於被中斷執行緒的程式碼檢測到中斷事件的發生
  • 執行緒終結規則:執行緒中所有的操作都先行發生於執行緒的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到執行緒已經終止執行
  • 物件終結規則:一個物件的初始化完成先行發生於他的finalize()方法的開始

 

Java Framework,歡迎各位前來交流java相關
QQ群:965125360

推薦JVM的視訊:

深入理解Java虛擬機器(jvm效能調優+記憶體模型+虛擬機器原理)
https://item.taobao.com/item.htm?spm=a1z38n.10677092.0.0.c9e51deb8seTn4&id=583526620411