一般來說內(nèi)存屏障分為兩層:編譯器屏障和CPU屏障削彬,前者只在編譯期生效,目的是防止編譯器生成亂序的內(nèi)存訪問指令;后者通過插入或修改特定的CPU指令,在運行時防止內(nèi)存訪問指令亂序執(zhí)行。
下面簡單說一下這兩種屏障惋增。
1、編譯器屏障
編譯器屏障如下:
asm
`volatile(
"": : :
"memory"``
內(nèi)聯(lián)匯編時只是插入了一個空指令""改鲫,關(guān)鍵在在內(nèi)聯(lián)匯編中的修改寄存器列表中指定了"memory"诈皿,它告訴編譯器:這條指令(其實是空的)可能會讀取任何內(nèi)存地址,也可能會改寫任何內(nèi)存地址像棘。那么編譯器會變得保守起來稽亏,它會防止這條fence命令上方的內(nèi)存訪問操作移到下方,同時防止下方的操作移到上面缕题,也就是防止了亂序截歉,是我們想要的結(jié)果。這條命令還有另外一個副作用:它會讓編譯器把所有緩存在寄存器中的內(nèi)存變量刷新到內(nèi)存中烟零,然后重新從內(nèi)存中讀取這些值瘪松。
總結(jié)一下就是,如上命令有兩個作用锨阿,防止指令重排序以及保證可見性宵睦。
如果使用純字節(jié)碼解釋器來運行Java,那么HotSpot VM中orderAccess_linux_zero.inline.hpp文件中有如下實現(xiàn):
static inline void compiler_barrier() {
__asm__ volatile("": : :"memory");
}
inline void OrderAccess::loadload() {
compiler_barrier();
}
inline void OrderAccess::storestore() {
compiler_barrier(); }
inline void OrderAccess::loadstore() {
compiler_barrier(); }
這種方式依賴于編譯器達(dá)到目的時墅诡,如果編譯器支持壳嚎,就不用在不同的平臺和CPU上再專門編寫對應(yīng)的實現(xiàn),簡化了跨平臺操作。
2诬辈、x86 CPU屏障
x86屬于一個強(qiáng)內(nèi)存模型,這意味著在大多數(shù)情況下CPU會保證內(nèi)存訪問指令有序執(zhí)行荐吉。為了防止這種CPU亂序焙糟,我們需要添加CPU內(nèi)存屏障。X86專門的內(nèi)存屏障指令是"mfence"样屠,另外還可以使用lock指令前綴起到相同的效果穿撮,后者開銷更小。也就是說痪欲,內(nèi)存屏障可以分為兩類:
- 本身是內(nèi)存屏障悦穿,比如“l(fā)fence”,“sfence”和“mfence”匯編指令
- 本身不是內(nèi)存屏障业踢,但是被lock指令前綴修飾栗柒,其組合成為一個內(nèi)存屏障。在X86指令體系中知举,其中一類內(nèi)存屏障常使用“l(fā)ock指令前綴加上一個空操作”方式實現(xiàn)瞬沦,比如lock addl $0x0,(%esp)
下面介紹一下lock指令前綴。lock指令前綴功能如下:
- 被修飾的匯編指令成為“原子的”
- 與被修飾的匯編指令一起提供內(nèi)存屏障效果
在X86指令體系中雇锡,具有l(wèi)ock指令前綴逛钻,其內(nèi)允許使用lock指令前綴修飾的匯編指令有:
ADD,ADC,AND,BTC,BTR,BTS,CMPXCHG,CMPXCH8B,DEC,INC,NEG,NOT,OR,SBB,SUB,XOR,XADD,XCHG等
需要注意的是,“XCHG”和“XADD”匯編指令本身是原子指令锰提,但也允許使用lock指令前綴進(jìn)行修飾曙痘。
lock前綴的2個作用要記住。第一個是內(nèi)存屏障立肘,任何顯式或隱式帶有l(wèi)ock前綴的指令以及CPUID等指令都有內(nèi)存屏障的作用边坤。如xchg [mem], reg具有隱式的lock前綴。第二個是原子性谅年,單指令并不是一個不可分割的操作惩嘉,比如mov,本身只有其操作數(shù)滿足某些條件的時候才是原子的踢故,但是如果允許有l(wèi)ock前綴文黎,那就是原子的。
3殿较、HotSpot VM中的內(nèi)存屏障
JMM為了更好讓Java開發(fā)者獨立于CPU的方式理解這些概念耸峭,對內(nèi)存讀(Load)和寫(Store)操作進(jìn)行兩兩組合:LoadLoad、LoadStore淋纲、StoreLoad以及StoreStore劳闹,只有StoreLoad組合可能亂序,而且Store和Load的內(nèi)存地址必須是不一樣的。
現(xiàn)在只討論x86架構(gòu)下的CPU屏障本涕,參考的是Intel手冊业汰。4個屏障只是Java為了跨平臺而設(shè)計出來的,實際上根據(jù)CPU的不同菩颖,對應(yīng) CPU 平臺上的 JVM 可能可以優(yōu)化掉一些 屏障样漆,例如LoadLoad、LoadStore和StoreStore是x86上默認(rèn)就有的行為晦闰,在這個平臺上寫代碼時會簡化一些開發(fā)過程放祟。X86-64下僅支持一種指令重排:StoreLoad ,即讀操作可能會重排到寫操作前面呻右,同時不同線程的寫操作并沒有保證全局可見跪妥,例子見《Intel? 64 and IA-32 Architectures Software Developer’s Manual》手冊8.6.1、8.2.3.7節(jié)声滥。這個問題用lock或mfence解決眉撵,不能靠組合sfence和lfence解決。
JDK 1.8版本中的HotSpot VM在x86上實現(xiàn)的loadload()落塑、storestore()以及l(fā)oadstore()函數(shù)如下:
inline` `void` `OrderAccess::loadload(){
acquire();
}
inline void OrderAccess::storestore(){
release();
}
inline void OrderAccess::loadstore(){
acquire();
}
inline void OrderAccess::storeload(){
fence();
}
inline void OrderAccess::acquire() {
volatile intptr_t local_dummy;
#ifdef AMD64
__asm__ volatile ("movq 0(%%rsp), %0" : "=r" (local_dummy) : : "memory");
#else
__asm__ volatile ("movl 0(%%esp),%0" : "=r" (local_dummy) : : "memory");
#endif // AMD64
}
inline void OrderAccess::release() {
// Avoid hitting the same cache-line from different threads.
volatile jint local_dummy = 0;
}
acquire語義防止它后面的讀寫操作重排序到acquire前面执桌,所以LoadLoad和LoadStore組合后可滿足要求;release防止它前面的讀寫操作重排序到release后面芜赌,所以可由StoreStore和LoadStore組合后滿足要求仰挣。這樣acquire和release就可以實現(xiàn)一個"柵欄",禁止內(nèi)部讀寫操作跑到外邊缠沈,但是外邊的讀寫操作仍然可以跑到“柵欄”內(nèi)膘壶。
在x86上,acquire和release沒有涉及到StoreLoad洲愤,所以本來默認(rèn)支持颓芭,在函數(shù)實現(xiàn)時,完全可以不做任何操作柬赐。具體在實現(xiàn)時亡问,acquire()函數(shù)讀取了一個C++的volatile變量,而release()函數(shù)寫入了一個C++的volatile變量肛宋。這可能是支持微軟從Visual Studio 2005開始就對C++ volatile關(guān)鍵字添加了同步語義州藕,也就是對volatile變量的讀操作具有acquire語義,對volatile變量的寫操作具有release語義酝陈。
另外還可以順便說一下床玻,借助acquire與release語義可以實現(xiàn)互斥鎖(mutex),實際上沉帮,mutex正是acquire與release這兩個原語的由來锈死,acquire的本意是acquire a lock贫堰,release的本意是release a lock,因此待牵,互斥鎖能保證被鎖住的區(qū)域內(nèi)得到的數(shù)據(jù)不會是過期的數(shù)據(jù)其屏,而且所有寫入操作在release之前一定會寫入內(nèi)存。所以后續(xù)我們在實現(xiàn)鎖的過程中會有如下代碼出現(xiàn):
pthread_mutex_lock(&mutex);
// 操作
pthread_mutex_unlock(&mutex);
OrderAccess::storeload()函數(shù)調(diào)用的fence()的實現(xiàn)如下:
inline void OrderAccess::fence() {
__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
}
可以看到是使用lock前綴來解決內(nèi)存屏障問題缨该。
下面看一下Java的volatile變量的實現(xiàn)偎行。
字節(jié)碼層面會在access_flags中會標(biāo)記某個屬性為volatitle,到HotSpot VM后压彭,對volatitle內(nèi)存區(qū)進(jìn)行讀寫時,都加屏障渗常,如讀取volatile變量時加如下屏障:
volatile變量讀操作
LoadLoad
LoadStore
在寫volatilie變量時加如下屏障:
LoadStore
StoreStore
volatile變量寫操作
StoreLoad
如上的volatile變量讀之后的操作不允許重排序到前面壮不,而寫之前的操作也不允許重排序到寫后面,所以volatile有acquire和release的語義皱碘。
對x86-64位來說询一,只需要對StoreLoad進(jìn)行處理,所以從解釋執(zhí)行的putfield或putstatic指令來看癌椿,會在最后寫入volatilie變量后加如下指令:
lock addl $0x0,(%rsp)
在多線程編程中健蕊,由于使用互斥量,信號量和事件都在設(shè)計的時候都阻止了它們調(diào)用點中的內(nèi)存亂序(已經(jīng)隱式包含各種memery barrier)踢俄,內(nèi)存亂序的問題同樣不需要考慮了缩功。只有當(dāng)使用無鎖(lock-free)技術(shù)時–內(nèi)存在線程間共享而沒有任何的互斥量,內(nèi)存亂序的效果才會顯露無疑都办,這樣我們才需要考慮在合適的地方加入合適的memery barrier嫡锌。