聊聊高并发(三十五)Java内存模型那些事(三)理解内存屏障
硬件層提供了一系列的內存屏障 memory barrier / memory fence(Intel的提法)來提供一致性的能力。拿X86平臺來說,有幾種主要的內存屏障
1. lfence,是一種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前綴實現了類似的能力,
1. 它先對總線/緩存加鎖,然后執行后面的指令,最后釋放鎖后會把高速緩存中的臟數據全部刷新回主內存。
2. 在Lock鎖住總線的時候,其他CPU的讀寫請求都會被阻塞,直到鎖釋放。Lock后的寫操作會讓其他CPU相關的cache line失效,從而從新從內存加載最新的數據。這個是通過緩存一致性協議做的。
?
再引用一下這篇文章中關于Lock前綴的更多的一些描述?多處理器多線程使用_asm lock指令能保證互斥嗎?
“從 P6 處理器開始,如果指令訪問的內存區域已經存在于處理器的內部緩存中,則“lock” 前綴并不將引線 LOCK 的電位拉低,而是鎖住本處理器的內部緩存,然后依靠緩存一致性協議保證操作的原子性。
IA32 CPU調用有lock前綴的指令,或者如xchg這樣的指令,會導致其它的CPU也觸發一定的動作來同步自己的Cache。
CPU的#lock引腳鏈接到北橋芯片(North Bridge)的#lock引腳,當帶lock前綴的執行執行時,北橋芯片會拉起#lock
電平,從而鎖住總線,直到該指令執行完畢再放開。 而總線加鎖會自動invalidate所有CPU對 _該指令涉及的內存_
的Cache,因此barrier就能保證所有CPU的Cache一致性。
?lock前綴(或cpuid、xchg等指令)使得本CPU的Cache寫入了內存,該寫入動作也會引起別的CPU invalidate其Cache。
IA32在每個CPU內部實現了Snoopying(BUS-Watching)技術,監視著總線上是否發生了寫內存操作(由某個CPU或DMA控
制器發出的),只要發生了,就invalidate相關的Cache line。 因此,只要lock前綴導致本CPU寫內存,就必將導致
所有CPU去invalidate其相關的Cache line。 ”
?
內存屏障的概念很好理解,不同硬件實現內存屏障的方式不同,Java內存模型屏蔽了這種底層硬件平臺的差異,由JVM來為不同的平臺生成相應的機器碼。
?
看一下volatile的實現
有些材料里說Java實現volatile的時候使用了類似mfence等內存屏障,但是我經過測試發現在X86平臺上volatile是用Lock前綴來實現的,測試的是JDK6和7。
下面來看一下volatile生成的匯編碼
寫volatile的時候生成匯編碼是?lock addl $0x0, (%rsp), 在寫操作之前使用了lock前綴,鎖住了總線和對應的地址,這樣其他的寫和讀都要等待鎖的釋放。當寫完成后,釋放鎖,把緩存刷新到主內存。
1. 讀volatile就很好理解了,不需要額外的匯編指令,CPU發現對應地址的緩存被鎖了,等待鎖的釋放,緩存一致性協議會保證它讀到最新的值。?
2. 只需要對寫volatile的使用用lock對總線加鎖就行了,這樣其他的讀、寫操作等待總線釋放才能繼續讀。Lock會讓其他CPU的緩存invalide,從內存重新加載數據。
?
再看一下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的方式修改對象頭的鎖標志位為”釋放“狀態,寫操作都立刻寫回主內存。JVM會進一步對synchronized時CAS失敗的那些線程進行阻塞操作,這部分的邏輯沒有體現在lock cmpxchg指令上,我猜想是通過某種信號量來實現的。lock cmpxchg指令前者保證了可見性和防止重排序,后者保證了操作的原子性。
?
參考資料:
Memory Barriers/Fences
LOCK vs MFENCE
Memory barrier
多處理器多線程使用_asm lock指令能保證互斥嗎?
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的聊聊高并发(三十五)Java内存模型那些事(三)理解内存屏障的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 聊聊高并发(三十三)Java内存模型那些
- 下一篇: 聊聊高并发(三十六)Java内存模型那些