揭開java記憶體模型的神祕面紗

JAVA一方發表於2020-01-12
java併發採用的是共享記憶體模型,執行緒之間的通訊對程式設計師來說是透明的,記憶體可見性問題很容易困擾著java程式設計師,今天我們就來揭開java記憶體模型的神祕面紗。  

在揭開面紗之前,我們需要認識幾個基礎概念:記憶體屏障(memory Barriers),指令重排序,happens-before規則,as-if-serial語義。

什麼是 Memory Barrier(記憶體屏障)? 

記憶體屏障,又稱記憶體柵欄,是一個CPU指令,基本上它是一條這樣的指令:

 1、保證特定操作的執行順序。  

2、影響某些資料(或則是某條指令的執行結果)的記憶體可見性。 

編譯器和CPU能夠重排序指令,保證最終相同的結果,嘗試優化效能。插入一條Memory Barrier會告訴編譯器和CPU:不管什麼指令都不能和這條Memory Barrier指令重排序。 

Memory Barrier所做的另外一件事是強制刷出各種CPU cache,如一個 Write-Barrier(寫入屏障)將刷出所有在 Barrier 之前寫入 cache 的資料,因此,任何CPU上的執行緒都能讀取到這些資料的最新版本。

揭開java記憶體模型的神祕面紗

這和java有什麼關係?volatile是基於Memory Barrier實現的。 

如果一個變數是volatile修飾的,JMM會在寫入這個欄位之後插進一個Write-Barrier指令,並在

揭開java記憶體模型的神祕面紗

讀這個欄位之前插入一個Read-Barrier指令。

這意味著,如果寫入一個volatile變數a,可以保證:

 1、一個執行緒寫入變數a後,任何執行緒訪問該變數都會拿到最新值。 

2、在寫入變數a之前的寫入操作,其更新的資料對於其他執行緒也是可見的。因為Memory Barrier會刷出cache中的所有先前的寫入。  

happens-before

從jdk5開始,java使用新的JSR-133記憶體模型,基於happens-before的概念來闡述操作之間的記憶體可見性。  

在JMM中,如果一個操作的執行結果需要對另一個操作可見,那麼這兩個操作之間必須要存在happens-before關係,這個的兩個操作既可以在同一個執行緒,也可以在不同的兩個執行緒中。 

與程式設計師密切相關的happens-before規則如下: 

1、程式順序規則:一個執行緒中的每個操作,happens-before於該執行緒中任意的後續操作。

2、監視器鎖規則:對一個鎖的解鎖操作,happens-before於隨後對這個鎖的加鎖操作。  

3、volatile域規則:對一個volatile域的寫操作,happens-before於任意執行緒後續對這個volatile域的讀。 

4、傳遞性規則:如果 A happens-before B,且 B happens-before C,那麼A happens-before C。  

注意:兩個操作之間具有happens-before關係,並不意味前一個操作必須要在後一個操作之前執行!僅僅要求前一個操作的執行結果,對於後一個操作是可見的,且前一個操作按順序排在後一個操作之前。 

指令重排序 

在執行程式時,為了提高效能,編譯器和處理器會對指令做重排序。但是,JMM確保在不同的編譯器和不同的處理器平臺之上,通過插入特定型別的Memory Barrier來禁止特定型別的編譯器重排序和處理器重排序,為上層提供一致的記憶體可見性保證。 

1、編譯器優化重排序:編譯器在不改變單執行緒程式語義的前提下,可以重新安排語句的執行順序。 

2、指令級並行的重排序:如果不存l在資料依賴性,處理器可以改變語句對應機器指令的執行順序。 

3、記憶體系統的重排序:處理器使用快取和讀寫緩衝區,這使得載入和儲存操作看上去可能是在亂序執行。  

資料依賴性 

如果兩個操作訪問同一個變數,其中一個為寫操作,此時這兩個操作之間存在資料依賴性。  

編譯器和處理器不會改變存在資料依賴性關係的兩個操作的執行順序,即不會重排序。 

as-if-serial 

不管怎麼重排序,單執行緒下的執行結果不能被改變,編譯器、runtime和處理器都必須遵守as-if-serial語義。

抽象結構

java執行緒之間的通訊由java記憶體模型(JMM)控制,JMM決定一個執行緒對共享變數(例項域、靜態域和陣列)的寫入何時對其它執行緒可見。  

從抽象的角度來看,JMM定義了執行緒和主記憶體Main Memory(堆記憶體)之間的抽象關係:執行緒之間的共享變數儲存在主記憶體中,每個執行緒都有自己的本地記憶體Local Memory(只是一個抽象概念,物理上不存在),儲存了該執行緒的共享變數副本。  

所以,執行緒A和執行緒B之前需要通訊的話,必須經過一下兩個步驟: 

1、執行緒A把本地記憶體中更新過的共享變數重新整理到主記憶體中。 

2、執行緒B到主記憶體中讀取執行緒A之前更新過的共享變數。  

在此我向大家推薦一個架構學習交流群。交流學習群號:874811168 裡面會分享一些資深架構師錄製的視訊錄影:有Spring,MyBatis,Netty原始碼分析,高併發、高效能、分散式、微服務架構的原理,JVM效能優化、分散式架構等這些成為架構師必備的知識體系。還能領取免費的學習資源,目前受益良多 

———————————————— 版權宣告:本文為CSDN博主「Java架構閒談」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處連結及本宣告。 原文連結:https://blog.csdn.net/qiyue683209/article/details/82778894



相關文章