標籤:記憶體屏障 memory barrier memory fence volatile
在聊聊高並發(三十三)從一致性(Consistency)的角度理解Java記憶體模型 我們說了硬體層提供了滿足某些一致性需求的能力,Java記憶體模型利用了硬體層提供的能力指定了一系列的文法和規則,讓Java開發人員可以隔絕這種底層的實現專註於並發邏輯的開發。這篇我們來看看硬體層是如何提供這些實現一致性需求的能力的。
硬體層提供了一系列的記憶體屏障 memory barrier / memory fence(Intel的提法)來提供一致性的能力。拿X86平台來說,有幾種主要的記憶體屏障
1. ifence,是一種Load Barrier 讀屏障
2. sfence, 是一種Store Barrier 寫屏障
3. mfence, 是一種全能型的屏障,具備ifence和sfence的能力
4. Lock首碼,Lock不是一種記憶體屏障,但是它能完成類似記憶體屏障的功能。Lock會對CPU匯流排和快取加鎖,可以理解為CPU指令級的一種鎖。它後面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令。
記憶體屏障有兩個能力:
1. 阻止屏障兩邊的指令重排序
2. 強制把寫緩衝區/快取中的髒資料等寫回主記憶體,讓緩衝中相應的資料失效
對Load Barrier來說,在讀指令前插入讀屏障,可以讓快取中的資料失效,重新從主記憶體載入資料
對Store Barrier來說,在寫指令之後插入寫屏障,能讓寫入緩衝的最新資料寫回到主記憶體
Lock首碼實現了類似的能力,它先對匯流排和緩衝加鎖,然後執行後面的指令,最後釋放鎖後會把快取中的髒資料全部重新整理回主記憶體。在Lock鎖住匯流排的時候,其他CPU的讀寫請求都會被阻塞,直到鎖釋放。
記憶體屏障的概念很好理解,不同硬體實現記憶體屏障的方式不同,Java記憶體模型屏蔽了這種底層硬體平台的差異,由JVM來為不同的平台產生相應的機器碼。
有些材料裡說Java實現volatile的時候使用了類似mfence等記憶體屏障,但是我經過測試發現在X86平台上volatile是用Lock首碼來實現的,測試的是JDK6和7。
下面來看一下volatile產生的彙編碼
寫volatile的時候產生彙編碼是 lock addl $0x0, (%rsp), 在寫操作之前使用了lock首碼,鎖住了匯流排和對應的地址,這樣其他的寫和讀都要等待鎖的釋放。當寫完成後,釋放鎖,把緩衝重新整理到主記憶體。
讀volatile就很好理解了,CPU發現對應地址的緩衝被鎖了,等待鎖的釋放,緩衝一致性協議會保證它讀到最新的值
再看一下synchronized的實現。synchronized塊產生JVM指令是monitorenter, monitorexit,最後產生的彙編指令是
lock cmpxchg %r15, 0x16(%r10) 和 lock cmpxchg %r10, (%r11)
cmpxchg是CAS的彙編指令,這裡的含義是先用lock指令對匯流排和緩衝上鎖,然後用cmpxchg CAS操作設定對象頭中的synchronized標誌位。CAS完成後釋放鎖,把緩衝重新整理到主記憶體。
所以synchronized的底層操作含義是先對對象頭的鎖標誌位用lock cmpxchg的方式設定成“鎖住“狀態,釋放鎖時,在用lock cmpxchg的方式修改對象頭的鎖標誌位為”釋放“狀態,寫操作都立刻寫回主記憶體
參考資料:
Memory Barriers/Fences
LOCK vs MFENCE
Memory barrier
聊聊高並發(三十五)理解記憶體屏障