java 記憶體模型-03-快取和重排序
快取
為了提升效能,JVM 做了 2 件事情。
快取+重排序
為什麼會出現執行緒可見性問題
要想解釋為什麼會出現執行緒可見性問題,需要從計算機處理器結構談起。
我們都知道計算機運算任務需要CPU和記憶體相互配合共同完成,其中CPU負責邏輯計算,記憶體負責資料儲存。
CPU要與記憶體進行互動,如讀取運算資料、儲存運算結果等。
由於記憶體和CPU的計算速度有幾個數量級的差距,為了提高CPU的利用率,現代處理器結構都加入了一層讀寫速度儘可能接近CPU運算速度的快取記憶體來作為記憶體與CPU之間的緩衝:
將運算需要使用的資料複製到快取中,讓CPU運算可以快速進行,計算結束後再將計算結果從快取同步到主記憶體中,這樣處理器就無須等待緩慢的記憶體讀寫了。
快取記憶體的引入解決了CPU和記憶體之間速度的矛盾,但是在多CPU系統中也帶來了新的問題:快取一致性。
在多CPU系統中,每個CPU都有自己的快取記憶體,所有的CPU又共享同一個主記憶體。
如果多個CPU的運算任務都涉及到主記憶體中同一個變數時,那同步回主記憶體時以哪個CPU的快取資料為準呢?這就需要各個CPU在資料讀寫時都遵循同一個協議進行操作。
處理器=》快取記憶體=》快取一致性協議=》主記憶體 處理器=》快取記憶體=》快取一致性協議=》主記憶體
參考上圖,假設有兩個執行緒A、B分別在兩個不同的CPU上執行,它們共享同一個變數X。
如果執行緒A對X進行修改後,並沒有將 X 更新後的結果同步到主記憶體,則變數X的修改對B執行緒是不可見的。
所以CPU與記憶體之間的快取記憶體就是導致執行緒可見性問題的一個原因。
另一個原因就是重排序。
重排序
目的
現在的CPU一般採用流水線來執行指令。
一個指令的執行被分成:取指、譯碼、訪存、執行、寫回、等若干個階段。然後,多條指令可以同時存在於流水線中,同時被執行。
指令流水線並不是序列的,並不會因為一個耗時很長的指令在“執行”階段呆很長時間,而導致後續的指令都卡在“執行”之前的階段上。
重排序的目的是為了效能。
Example
理想情況下:
過程A:cpu0—寫入1—> bank0; 過程B:cpu0—寫入2—> bank1;
如果bank0狀態為busy, 則A過程需要等待
如果進行重排序,則直接可以先執行B過程。
類別
在執行程式時為了提高效能,編譯器和處理器常常會對指令做重排序。重排序分三種型別:
編譯器最佳化的重排序
編譯器在不改變單執行緒程式語義的前提下,可以重新安排語句的執行順序。
指令級並行的重排序
現代處理器採用了指令級並行技術(Instruction-Level Parallelism, ILP)來將多條指令重疊執行。
如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序。
記憶體系統的重排序
由於處理器使用快取和讀/寫緩衝區,這使得載入和儲存操作看上去可能是在亂序執行。
執行過程
從java原始碼到最終實際執行的指令序列,會分別經歷下面三種重排序:
{原始碼 -> 編譯器最佳化重排序(1) -> 指令級並行重排序(2) -> 記憶體系統重排序(3) -> 最終執行的指令順序}
上述的1屬於編譯器重排序,2和3屬於處理器重排序。這些重排序都可能會導致多執行緒程式出現記憶體可見性問題。
對於編譯器,JMM的編譯器重排序規則會禁止特定型別的編譯器重排序(不是所有的編譯器重排序都要禁止)。
對於處理器重排序,JMM的處理器重排序規則會要求java編譯器在生成指令序列時,插入特定型別的記憶體屏障(memory barriers,intel稱之為memory fence)指令,
透過記憶體屏障
指令來禁止特定型別的處理器重排序(不是所有的處理器重排序都要禁止)。
JMM 屬於語言級的記憶體模型,它確保在不同的編譯器和不同的處理器平臺之上,透過禁止特定型別的編譯器重排序和處理器重排序,為程式設計師提供一致的記憶體可見性保證。
不進行重排序的場景
資料依賴性
如果兩個操作訪問同一個變數,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在資料依賴性。
| 名稱 | 示例 | 說明 |
| 寫後讀 | a = 1; b = a; | 寫一個變數後再讀這個位置 |
| 寫後寫 | a = 1; a = 2; | 寫一個變數後再寫這個變數 |
| 讀後寫 | a = b; b = 1; | 讀一個變數後再寫這個變數 |
上面三種情況,只要重排序兩個操作的執行順序,程式的執行結果將會被改變。
所以有資料依賴性的語句不能進行重排序。
as-if-serial
概念
as-if-serial語義就是: 不管怎麼重排序(編譯器和處理器為了提高並行度), 單執行緒程式的執行結果不能被改變。所以編譯器和cpu進行指令重排序時候回遵守as-if-serial語義。
栗子
public int add() { int x = 1; //1 int y = 1; //2 int ans = x + y; //3 return ans }
上面三條指令, 指令1和指令2沒有資料依賴關係, 指令3依賴指令1和指令2。
根據上面我們講的重排序不會改變我們的資料依賴關係, 依據這個結論, 我們可以確信指令3是不會重排序於指令1和指令2的前面。
我們看一下上面上條指令編譯成位元組碼檔案之後
public int add(); Code: 0: iconst_1 // 將int型數值1入運算元棧 1: istore_1 // 將運算元棧頂數值寫到區域性變數表的第2個變數(因為非靜態方法會傳入this, this就是第一個變數) 2: iconst_1 // 將int型數值1入運算元棧 3: istore_2 // 將將運算元棧頂數值寫到區域性變數表的第3個變數 4: iload_1 // 將第2個變數的值入運算元棧 5: iload_2 // 將第三個變數的值入運算元棧 6: iadd // 運算元棧頂元素和棧頂下一個元素做int型add操作, 並將結果壓入棧 7: istore_3 // 將棧頂的數值存入第四個變數 8: iload_3 // 將第四個變數入棧 9: ireturn // 返回
作者:葉止水
連結:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2730/viewspace-2818144/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- java記憶體模型——重排序Java記憶體模型排序
- JVM記憶體結構、Java記憶體模型和Java物件模型JVM記憶體Java模型物件
- Java記憶體區域和記憶體模型Java記憶體模型
- CPU快取和記憶體屏障快取記憶體
- 淺談JVM記憶體結構 和 Java記憶體模型 和 Java物件模型JVM記憶體Java模型物件
- Java記憶體快取-通過Google Guava建立快取Java記憶體快取GoGuava
- 深入理解Java記憶體模型(二)——重排序Java記憶體模型排序
- 建立快取記憶體機制-java版快取記憶體Java
- 【死磕Java併發】-----Java記憶體模型之重排序Java記憶體模型排序
- CPU快取記憶體快取記憶體
- Java記憶體快取-通過Map定製簡單快取Java記憶體快取
- Java記憶體模型Java記憶體模型
- Java 記憶體模型Java記憶體模型
- 多核cpu、cpu快取記憶體、快取一致性協議、快取行、記憶體快取記憶體協議
- 記憶體快取選型記憶體快取
- 第三章 Java記憶體模型之重排序④Java記憶體模型排序
- 什麼是Java記憶體模型(JMM)中的主記憶體和本地記憶體?Java記憶體模型
- Java記憶體模型(MESI、記憶體屏障、volatile和鎖及final記憶體語義)Java記憶體模型
- Java記憶體模型FAQ(一) 什麼是記憶體模型Java記憶體模型
- 探索Java記憶體模型Java記憶體模型
- 理解Java記憶體模型Java記憶體模型
- JMM Java 記憶體模型Java記憶體模型
- Java記憶體模型-(1)Java記憶體模型
- Java物件記憶體模型Java物件記憶體模型
- Java的記憶體模型Java記憶體模型
- 直接記憶體和堆記憶體誰快記憶體
- docker部署redis快取記憶體DockerRedis快取記憶體
- 談談CPU快取記憶體快取記憶體
- django 快取表格到記憶體Django快取記憶體
- Java記憶體模型FAQ(四)重排序意味著什麼?Java記憶體模型排序
- Java記憶體模型是什麼,為什麼要有Java記憶體模型,Java記憶體模型解決了什麼問題?Java記憶體模型
- 淺談Java記憶體模型Java記憶體模型
- Java記憶體模型之前奏Java記憶體模型
- Java記憶體模型簡介Java記憶體模型
- Java記憶體模型 - 簡介Java記憶體模型
- Concurrency(五: Java記憶體模型)Java記憶體模型
- JAVA記憶體模型和Happens-Before規則Java記憶體模型APP
- MRAM快取記憶體的組成快取記憶體