1. 程式人生 > 其它 >jvm記憶體模型:as-if-serial和happen-before

jvm記憶體模型:as-if-serial和happen-before

參考連結:[https://blog.csdn.net/ma_chen_qq/article/details/82990603]
https://blog.csdn.net/ThinkWon/article/details/102074107


java記憶體模型是共享記憶體的併發模型,執行緒之間主要通過讀-寫共享變數來完成隱式通訊。java中的共享變數是儲存在記憶體中的,多個執行緒由其工作記憶體,其工作方式是將共享記憶體中的變數拿出來放在工作記憶體,操作完成後,再將最新的變數放回共享變數,這時其他的執行緒就可以獲取到最新的共享變數。
從橫向去看看,執行緒A和執行緒B就好像通過共享變數在進行隱式通訊。這其中有很有意思的問題,如果執行緒A更新後資料並沒有及時寫回到主存,而此時執行緒B讀到的是過期的資料,這就出現了 “髒讀” 現象。
為避免髒讀,可以通過同步機制(控制不同執行緒間操作發生的相對順序)來解決或者通過volatile關鍵字使得每次volatile變數都能夠強制重新整理到主存,從而對每個執行緒都是可見的。

重排序
在執行程式時,為了提高效能,編譯器和處理器常常會對指令進行重排序。一般重排序可以分為如下三種:如圖,1屬於編譯器重排序,而2和3統稱為處理器重排序。
這些重排序會導致執行緒安全的問題,一個很經典的例子就是DCL問題。JMM的編譯器重排序規則會禁止一些特定型別的編譯器重排序;針對處理器重排序,編譯器在生成指令序列的時候會通過插入記憶體屏障指令來禁止某些特殊的處理器重排序。

(1)編譯器優化的重排序。編譯器在不改變單執行緒程式語義的前提下,可以重新安排語句的執行順序;
(2)指令級並行的重排序。現代處理器採用了指令級並行技術來將多條指令重疊執行。如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序;
(3)記憶體系統的重排序。由於處理器使用快取和讀/寫緩衝區,這使得載入和儲存操作看上去可能是在亂序執行的。

as-if-serial規則
as-if-serial語義的意思指:不管怎麼重排序(編譯器和處理器為了提高並行度),(單執行緒)程式的執行結果不能被改變。編譯器,runtime 和處理器都必須遵守as-if-serial語義。

為了遵守as-if-serial語義,編譯器和處理器不會對存在資料依賴關係的操作做重排序,因為這種重排序會改變執行結果。但是,如果操作之間不存在資料依賴關係,這些操作可能被編譯器和處理器重排序。為了具體說明,請看下面計算圓面積的程式碼示例:


double pi  = 3.14;    //A
double r   = 1.0;     //B
double area = pi * r * r; //C
1
2
3

A和C之間存在資料依賴關係,同時B和C之間也存在資料依賴關係。因此在最終執行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程式的結果將會被改變)。但A和B之間沒有資料依賴關係,編譯器和處理器可以重排序A和B之間的執行順序。as-if-serial語義把單執行緒程式保護了起來,遵守as-if-serial語義的編譯器,runtime 和處理器共同讓編寫單執行緒程式的程式設計師產生了一個幻覺:單執行緒程式是按程式的順序來執行的as-if-serial語義使程式設計師無需擔心重排序會干擾他們,也無需擔心記憶體可見性問題。

happens-before規則
上面的內容講述了重排序原則,一會是編譯器重排序一會是處理器重排序,如果讓程式設計師再去了解這些底層的實現以及具體規則,那麼程式設計師的負擔就太重了,嚴重影響了併發程式設計的效率。因此,JMM為程式設計師在上層提供了六條規則,這樣我們就可以根據規則去推論跨執行緒的記憶體可見性問題,而不用再去理解底層重排序的規則。

happens-before定義
happens-before的概念最初由Leslie Lamport在其一篇影響深遠的論文(《Time,Clocks and the Ordering of Events in a Distributed System》)中提出,有興趣的可以google一下。JSR-133使用happens-before的概念來指定兩個操作之間的執行順序。由於這兩個操作可以在一個執行緒之內,也可以是在不同執行緒之間。因此,JMM可以通過happens-before關係向程式設計師提供跨執行緒的記憶體可見性保證(如果A執行緒的寫操作a與B執行緒的讀操作b之間存在happens-before關係,儘管a操作和b操作在不同的執行緒中執行,但JMM向程式設計師保證a操作將對b操作可見)。具體的定義為:

1)如果一個操作happens-before另一個操作,那麼第一個操作的執行結果將對第二個操作可見,而且第一個操作的執行順序排在第二個操作之前。

2)兩個操作之間存在happens-before關係,並不意味著Java平臺的具體實現必須要按照happens-before關係指定的順序來執行。如果重排序之後的執行結果,與按happens-before關係來執行的結果一致,那麼這種重排序並不非法(也就是說,JMM允許這種重排序)。

上面的1)是JMM對程式設計師的承諾。

從程式設計師的角度來說,可以這樣理解happens-before關係:如果A happens-before B,那麼Java記憶體模型將向程式設計師保證——A操作的結果將對B可見,且A的執行順序排在B之前。注意,這只是Java記憶體模型向程式設計師做出的保證!

上面的2)是JMM對編譯器和處理器重排序的約束原則。

正如前面所言,JMM其實是在遵循一個基本原則:只要不改變程式的執行結果(指的是單執行緒程式和正確同步的多執行緒程式),編譯器和處理器怎麼優化都行。JMM這麼做的原因是:程式設計師對於這兩個操作是否真的被重排序並不關心,程式設計師關心的是程式執行時的語義不能被改變(即執行結果不能被改變)。因此,happens-before關係本質上和as-if-serial語義是一回事。

具體規則
具體的一共有六項規則:

  • 程式順序規則:一個執行緒中的每個操作,happens-before於該執行緒中的任意後續操作。
  • 監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖。
  • volatile變數規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。
  • 傳遞性:如果A happens-before B,且B happens-before C,那麼A happens-before C。
  • start()規則:如果執行緒A執行操作ThreadB.start()(啟動執行緒B),那麼A執行緒的ThreadB.start()操作happens-before於執行緒B中的任意操作。
  • join()規則:如果執行緒A執行操作ThreadB.join()併成功返回,那麼執行緒B中的任意操作happens-before於執行緒A從ThreadB.join()操作成功返回。
  • 程式中斷規則:對執行緒interrupted()方法的呼叫先行於被中斷執行緒的程式碼檢測到中斷時間的發生。
  • 物件finalize規則:一個物件的初始化完成(建構函式執行結束)先行於發生它的finalize()方法的開始。

下面以一個具體的例子來講下如何使用這些規則進行推論:

double pi  = 3.14;    //A
double r   = 1.0;     //B
double area = pi * r * r; //C

依舊以上面計算圓面積的進行描述。利用程式順序規則(規則1)存在三個happens-before關係:1. A happens-before B;2. B happens-before C;3. A happens-before C。這裡的第三個關係是利用傳遞性進行推論的。A happens-before B,定義1要求A執行結果對B可見,並且A操作的執行順序在B操作之前,但與此同時利用定義中的第二條,A,B操作彼此不存在資料依賴性,兩個操作的執行順序對最終結果都不會產生影響,在不改變最終結果的前提下,允許A,B兩個操作重排序,即happens-before關係並不代表了最終的執行順序。

as-if-serial與happens-before的區別
下面來比較一下as-if-serial和happens-before:

  • as-if-serial語義保證單執行緒內程式的執行結果不被改變,happens-before關係保證正確同步的多執行緒程式的執行結果不被改變。

  • as-if-serial語義給編寫單執行緒程式的程式設計師創造了一個幻境:單執行緒程式是按程式的順序來執行的。happens-before關係給編寫正確同步的多執行緒程式的程式設計師創造了一個幻境:正確同步的多執行緒程式是按happens-before指定的順序來執行的。

  • as-if-serial語義和happens-before這麼做的目的,都是為了在不改變程式執行結果的前提下,儘可能地提高程式執行的並行度。