Java內存模型精講

1.JAVA 的併發模型

共享內存模型java

       在共享內存的併發模型裏面,線程之間共享程序的公共狀態,線程之間經過讀寫內存中公共狀態來進行隱式通訊數組

       該內存指的是主內存,其實是物理內存的一小部分緩存

2.JAVA 內存模型的抽象

2.1 java內存中哪些數據是線程安全的,哪些是非安全的

  1. 非線程安全 : 在 java 中全部的實例域、靜態域、和數組元素都存放在堆內存中,而且這些數據是線程共享的,因此會存在內存可見性問題
  2. 線程安全 : 局部變量、方法定義的參數、異常處理器參數是當前線程虛擬機棧中的數據,而且不會進行線程共享,因此不會存在內存可見性問題安全

    2.2 線程間通信的本質

  3. 線程間通信的本質是 :JMM即 JAVA 內存模型進行控制,JMM決定了一個線程對共享變量的寫入什麼時候對其餘線程可見。

圖片

由上圖能看出來線程間的通信都是經過主內存來進行傳遞消息的, 每一個線程在進行共享數據處理的時候都是將共享的數據複製到當前線程本地(每一個線程本身都有一個內存)來進行操做。多線程

  1. 消息通信過程(不考慮數據安全性的問題) :
    • 線程一將主內存中的共享變量 A 加載到本身的本地內存中進行處理。好比 A = 1;
    • 此時將修改的共享變量 A 刷入到主內存中, 以後線程二再將主內存中的共享變量 A 讀取到本地內存進行操做;

整個數據交互的過程是JMM控制的,主要控制主內存與每一個線程的本地內存如何進行交互來提供共享數據的可見性併發

3.重排序

程序在執行的時候爲了提升效率會將程序指令進行從新排序app

3.1 重排序分類

  • 編譯器優化重排序

編譯器在不改變單線程程序語義的狀況下進行語句執行順序的優化ide

  • 指令集並行重排序

若是不存在數據的依賴性的話,處理器能夠改變語句對應機器指令的執行順序優化

  • 內存系統重排序

因爲處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操做看上去多是在亂序執行線程

3.2 重排序過程

圖片

以上三種重排序都會致使咱們在寫併發程序的時候出現內存可見性的問題。

JMM的編譯器重排序規則會禁止特定類型的編譯器重排序;

JMM的處理器重排序規則會要求java編譯器在生成指令序列的時候插入特定的內存屏障指令,經過內存屏障指令來禁止特定類型的處理器進行重排序

3.3 處理器重排序

因爲爲了不處理器等待向內存中寫入數據的延時,在處理器和內存中間加了一個緩衝區,這樣處理器能夠一直向緩衝區中寫入數據,等到必定時間將緩衝區的數據一次性的刷入到內存中。

優勢 :

  1. 處理器不一樣停頓,提升了處理器的運行效率
  2. 減小在向內存寫入數據時的內存總線的佔用

缺點 :

  1. 每一個處理器上的寫緩衝區只對當前處理器可見,因此就會形成內存操做的執行順序和實際狀況不符合

例如如下場景 :

圖片

       在當前場景中就可能出如今處理器 A 和處理器 B 沒有將它們各自的寫緩衝區中的數據刷回內存中, 將內存中讀取的A = 0、B = 0 進行給X和Y賦值,此時將緩衝區的數據刷入內存,致使了最後結果和實際想要的結果不一致。由於只有將緩衝區的數據刷入到了內存中才叫真正的執行

       以上主內存與工做內存之間的具體交互協議,即一個變量如何從主內存拷貝到工做內存,如何從工做內存同步到主內存之間的實現細節,JMM定義瞭如下8種操做來完成

操做 語義解析
lock(鎖定) 做用於主內存的變量,把一個變量標記爲一條線程獨佔狀態
unlock(解鎖) 做用於主內存的變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其餘線程鎖定
read(讀取) 做用於主內存的變量,把一個變量值從主內存傳輸到線程的工做內存中,以便隨後的load動做使用
load(載入) 做用於工做內存的變量,它把read操做從主內存中獲得的變量值放入工做內存的變量副本中
use(使用) 做用於工做內存的變量,把工做內存中的一個變量值傳遞給執行引擎
assign(賦值) 做用於工做內存的變量,它把一個從執行引擎接收到的值賦給工做內存的變量
store(存儲) 做用於工做內存的變量,把工做內存中的一個變量的值傳送到主內存中,<br>以便隨後的write的操做
write(寫入) 做用於工做內存的變量,它把store操做從工做內存中的一個變量的值傳送<br>到主內存的變量中

       若是要把一個變量從主內存中複製到工做內存中,就須要按順序地執行read和load操做,若是把變量從工做內存中同步到主內存中,就須要按順序地執行store和write操做。但Java內存模型只要求上述操做必須按順序執行,而沒有保證必須是連續執行

操做執行流程圖解:

圖片

同步規則分析

  1. 不容許一個線程無緣由地(沒有發生過任何assign操做)把數據從工做內存同步回主內存中
  2. 一個新的變量只能在主內存中誕生,不容許在工做內存中直接使用一個未被初始化(load或者assign)的變量。即就是對一個變量實施use和store操做以前,必須先自行assign和load操做。
  3. 一個變量在同一時刻只容許一條線程對其進行lock操做,但lock操做能夠被同一線程重複執行屢次,屢次執行lock後,只有執行相同次數的unlock操做,變量纔會被解鎖。lock和unlock必須成對出現。
  4. 若是對一個變量執行lock操做,將會清空工做內存中此變量的值,在執行引擎使用這個變量以前須要從新執行load或assign操做初始化變量的值。
  5. 若是一個變量事先沒有被lock操做鎖定,則不容許對它執行unlock操做;也不容許去unlock一個被其餘線程鎖定的變量。
  6. 對一個變量執行unlock操做以前,必須先把此變量同步到主內存中(執行store和write操做)

    3.4 內存屏障指令

       爲了解決處理器重排序致使的內存錯誤,java編譯器在生成指令序列的適當位置插入內存屏障指令,來禁止特定類型的處理器重排序

內存屏障指令

屏障類型 指令示例 說明
LoadLoadBarriers Load1;LoadLoad;Load2 Load1數據裝載發生在Load2及其全部後續數據裝載以前
StoreStoreBarriers Store1;StoreStore;Store2 Store1數據刷回主存要發生在Store2及其後續全部數據刷回主存以前
LoadStoreBarriers Load1;LoadStore;Store2 Load1數據裝載要發生在Store2及其後續全部數據刷回主存以前
StoreLoadBarriers Store1;StoreLoad;Load2 Store1數據刷回內存要發生在Load2及其後續全部數據裝載以前

3.5 happens-before(先行規則)

       happens-before 原則來輔助保證程序執行的原子性、可見性以及有序性的問題,它是判斷數據是否存在競爭、線程是否安全的依據

       在JMM中若是一個操做中的結果須要對另外一個操做可見,那麼這兩個操做以前必需要存在happens-before關係 (兩個操做能夠是同一個線程也能夠不是一個線程)

規則內容:

  • 程序順序規則 : 指的是在一個線程內控制代碼順序,好比分支、循環等,即在一個線程內必須保證語義串行性,也就是說按照代碼順序執行
  • 加鎖規則 : 一個解鎖(unlock)操做必定要發生於一個加鎖(lock)操做以前,也就是說,若是對於一個鎖解鎖後,再加鎖,那麼加鎖的動做必須在解鎖動做以後(同一個鎖)
  • volatile變量規則 : 對一個volatile的變量的寫操做要發生在對這個變量的讀操做以前,這保證了volatile變量的可見性,簡單的理解就是,volatile變量在每次被線程訪問時,都強迫從主內存中讀該變量的值,而當該變量發生變化時,又會強迫將最新的值刷新到主內存,任什麼時候刻,不一樣的線程老是可以看到該變量的最新值
  • 線程啓動規則 : 線程的啓動方法 start() 要發生在當前線程全部操做以前
  • 線程終止規則 : 線程中全部的操做都要發生在線程終止以前,Thread.join()方法的做用是等待當前執行的線程終止。假設在線程B終止以前,修改了共享變量,線程A從線程B的join方法成功返回後,線程B對共享變量的修改將對線程A可見
  • 線程中斷規則 : 線程調用interrupt()方法要發生在被中斷線程的代碼檢查出中斷事件以前
  • 對象終結規則 : 對象的初始化完成要發生在對象被回收以前
  • 傳遞性規則 : 若是操做 A 發生在操做 B 以前,操做 B 又發生在操做 C 以前,那麼操做A必定發生於操做 C 以前

注意: 兩個操做之間具備 happens-before 關係,並不意味着前一個操做必需要在後一個操做以前執行,只須要前一個操做的結果對後一個操做可見,而且前一個操做按順序要排在後一個操做以前。

3.6 數據依賴性

       就是前一個操做的結果對後一個操做的結果產生影響,此時編譯器和處理器在處理當前有數據依賴性的操做時不會改變存在數據依賴的兩個操做的執行順序

注意: 此時所說的數據依賴僅僅針對單個處理器中執行的指令序列或者單個線程中執行的操做。不一樣處理器和不一樣線程的狀況編譯器和處理器是不會考慮的

3.7 as-if-serial

       在單線程狀況下無論怎麼重排序程序的執行結果不能被改變,因此若是在單處理器或者單線程的狀況下,編譯器和處理器對於有數據依賴性的操做是不會進行重排序的。反之若是沒有數據依賴性的操做就有可能發生指令重排。

4.數據競爭與順序一致性

在多線程狀況下才會出現數據競爭

4.1 數據競爭

       在一個線程中寫了一個變量,在另外一個線程中讀一個變量,並且寫和讀並沒有進行同步

4.2 順序一致性

       若是在多線程條件下,程序可以正確的使用同步機制,那麼程序的執行將具備順序一致性(就像在單線程條件下執行同樣) 程序最終運行的結果與你預期的結果同樣

4.3 順序一致性內存模型

4.3.1特性:

  • 一個線程中的全部操做必須按照程序的順序來執行
  • 全部的操做都必須是原子性的操做,而且對其餘線程可見的

4.3.2概念:

       在概念上,順序一致性有一個單一的全局內存,在任意時間點最多隻有一個線程能夠鏈接到內存,當在多線程的場景下,會把全部內存的讀寫操做變成串行化

4.3.3案例:

       例若有多個併發線程 A B C, A 線程有兩個操做 A1 A2, 他們的執行的順序是 A1 -> A2 。B 線程有三個操做 B1 B2 B3, 他們的執行的順序是 B1 -> B2 ->B3 。C 線程有兩個操做 C1 C2 那麼他們在程序中執行的順序是 C1 -> C2 。

場景分析 :

場景一 : 併發安全(同步)執行順序

A1 -> A2 -> B1 -> B2 ->B3 -> C1 -> C2

場景二: 併發不安全(非同步)執行順序

A1 -> B1 -> A2 -> C1 -> B2 ->B3 -> C2

結論 :

       在非同步的場景下,即便三個線程中的每個操做亂序執行,可是在每一個線程中的各自操做仍是保持有序的。而且全部線程都只能看到一個一致的總體執行順序,也就是說三個線程看到的都是該順序 : A1 -> B1 -> A2 -> C1 -> B2 ->B3 -> C2 ,由於順序一致性內存模型中的每一個操做必須當即對任意線程可見。

       以上案例場景在JMM中不是這樣的,未同步的程序在JMM中不只總體的執行順序變了,就連每一個線程的看到的操做執行順序也是不同的

       例如前面所說的若是線程A將變量的值 a = 2 寫入到了本身的本地內存中,尚未刷入到主存中,在線程 A 來看值是變了,可是其餘線程 B 線程 C 根本看不到值的改變,就認爲線程A 的操做尚未發生,只有線程 A 將工做內存中的值刷回主內存線程 B和線程C 才能的到。可是若是是同步的狀況下,順序一致性模型和JMM模型執行的結果是一致的,可是程序的執行順序不必定,由於在JMM中,會發生指令重排現象因此執行順序會不一致

相關文章
相關標籤/搜索