聊聊高并发(三十六)Java内存模型那些事(四)理解Happens-before规则
在前幾篇將Java內(nèi)存模型的那些事基本上把這個(gè)域底層的概念都解釋清楚了,聊聊高并發(fā)(三十五)Java內(nèi)存模型那些事(三)理解內(nèi)存屏障?這篇分析了在X86平臺(tái)下,volatile,synchronized, CAS操作都是基于Lock前綴的匯編指令來實(shí)現(xiàn)的,關(guān)于Lock指令有兩個(gè)要點(diǎn):
1. lock會(huì)鎖總線,總線是互斥的,所以lock后面的寫操作會(huì)寫入緩存和內(nèi)存,可以理解為在lock后面的寫緩存和寫內(nèi)存這兩個(gè)動(dòng)作稱為了一個(gè)原子操作。當(dāng)總線被鎖時(shí),其他的CPU是無(wú)法使用總線的,也就讓其他的讀寫都等待lock的釋放
2. Lock寫完后,發(fā)起它的CPU的緩存和內(nèi)存都是最新值,其他CPU相關(guān)的緩存行都會(huì)invalidate,后續(xù)的讀/寫就會(huì)發(fā)生緩存不命中,從內(nèi)存重新加載最新值。
?
這里有個(gè)隱含的點(diǎn),我沒找到具體的資料,但是按照很多資料的說法:?volatile的寫操作相當(dāng)于釋放鎖,volatile的讀操作相當(dāng)于進(jìn)入鎖可以做下面的推斷:
volatile操作的是一個(gè)變量,而鎖保護(hù)的程序段中涉及到的變量可以是多個(gè),既然兩者的效果是一樣的,那么很可能lock后面的寫會(huì)讓高速緩存/寫緩存區(qū)的所有臟數(shù)據(jù)都刷新回主存。只有這樣volatile在可見性方面和鎖保護(hù)的程序段的可見性才是行為一致的。
?
理解這個(gè)很重要,因?yàn)楹瓦@篇講的Happens-before傳遞性有關(guān)系。Happens-before剛看到的時(shí)候從語(yǔ)言上看很難理解,覺得是廢話,但是它實(shí)際描述的問題其實(shí)是可見性的問題,順帶著有一些由于防止重排序而帶來的有序性的問題。聊聊高并發(fā)(三十三)Java內(nèi)存模型那些事(一)從一致性(Consistency)的角度理解Java內(nèi)存模型?這篇說了,內(nèi)存模型是一致性這個(gè)問題域里面的,一致性問題只涉及到了可見性和有序性這兩種特性,不包含原子性,所以Happens-before實(shí)際上是一系列的一致性的約束,所以它涉及到了可見性和有序性的意思,但沒有原子性的含義。
?
happens-before俗解?這篇文章已經(jīng)寫的很清楚了,我這邊再結(jié)合上一篇內(nèi)存屏障的一些概念錦上添花一下,進(jìn)一步說明這個(gè)問題
?
下面這些Happens-before的規(guī)則是從JSR 133 (Java Memory Model) FAQ?摘出來的,一條條看
?
- Each action in a thread happens before every action in that thread that comes later in the program's order.
- 可以理解為對(duì)于單個(gè)線程來說,前面的寫操作對(duì)后面都是可見的,這里肯定有人問那指令重排序之后怎么保證這點(diǎn)呢,我也有這個(gè)疑問,所以我理解的是如果這個(gè)寫是同步的,那么對(duì)單線程來說,所有同步的寫都是按照program order的,這個(gè)也是順序一致性的第一層含義。要理解的是,Java在使用了同步手段之后,被同步保護(hù)的點(diǎn)都是保證順序一致性的。因?yàn)橥降牡讓訉?shí)現(xiàn)比如內(nèi)存屏障 / lock都有防止重排序的含義
?
?
- An unlock on a monitor happens before every subsequent lock on?that same?monitor.
- 可以理解為一個(gè)鎖的釋放后它前面的寫操作對(duì)后續(xù)進(jìn)入同一個(gè)鎖的線程可見,對(duì)鎖來說這個(gè)太肯定了,釋放時(shí)會(huì)lock cmpxchg一次,進(jìn)入時(shí)會(huì)lock cmpxchg一次,兩次都保證了可見性
?
- A write to a volatile field happens before every subsequent read of?that same?volatile.
- 可以理解為volatile的寫操作對(duì)后續(xù)的讀可見,也是lock addl操作保證了寫volatile的可見性
?
- A call to?start()?on a thread happens before any actions in the started thread.
- 可以理解為線程start()寫線程開始狀態(tài)對(duì)后續(xù)線程的其他動(dòng)作可見,JVM內(nèi)部處理了,實(shí)際實(shí)現(xiàn)肯定也是用了lock/內(nèi)存屏障來實(shí)現(xiàn)的,其實(shí)在聊聊JVM(九)理解進(jìn)入safepoint時(shí)如何讓Java線程全部阻塞?中我們提到了線程狀態(tài)的改變,在JVM里面是對(duì)一個(gè)線程狀態(tài)變量進(jìn)行原子的修改,這個(gè)狀態(tài)的改變是原子的,并且可見的,當(dāng)然就具備了Happens-before的能力
?
- All actions in a thread happen before any other thread successfully returns from a?join()?on that thread.
- 可以理解為一個(gè)被join的線程中所有的寫操作在它join結(jié)束后回到原來的線程時(shí),對(duì)原來的線程可見。這個(gè)和上面的原理差不多,就是JVM在修改線程狀態(tài)的時(shí)候是一次原子操作,并且保證了可見性(估計(jì)是一次CAS),所以連帶著修改狀態(tài)前面的修改也都對(duì)后續(xù)的操作可見了
?
其他還有一些Happens-before規(guī)則,比如CAS操作,原子變量的修改都有Happens-before的含義,另外Happens-before具備傳遞性,比如 A happens beofre B, B happens before C, 那么A肯定 happens before C。
為什么具備傳遞性呢,原因還是在開篇的時(shí)候說的,lock/內(nèi)存屏障不僅僅把當(dāng)前的地址的數(shù)據(jù)原子的寫到緩存和內(nèi)存,肯定也把這之前CPU緩存/write buffer的臟數(shù)據(jù)寫回到主內(nèi)存了,這樣就實(shí)現(xiàn)了Happens before的傳遞性。
?
所以所有用到volatile ,synchronized, CAS的地方都具備Happens before的傳遞性,顯式鎖和原子變量底層都是基于CAS來實(shí)現(xiàn)的,當(dāng)然用到它們的時(shí)候也具備了Happens before的傳遞性。
?
所以下面這個(gè)例子就很好理解了,比如?y是volatile變量或者是原子變量/同步器類等等用到CAS的
線程A ? ?? 線程B??
x = 1??????? a = y
y = 2 ? ? ?? b = x
?
如果在時(shí)間順序上y=2這個(gè)對(duì)被同步的變量的寫先發(fā)生于 a = y 這個(gè)對(duì)被同步的變量的讀,那么可以肯定的說 b = x = 1。
有人問 x = 1會(huì)不會(huì)被重排到 y =2 之后,答案是不會(huì),因?yàn)閥是個(gè)被同步的變量,防止重排序, x 不會(huì)跨越內(nèi)存屏障排到y(tǒng)=2之后,所以
b = x同樣也不會(huì)被重排序到 a = y前面,因?yàn)?y是被同步的變量,內(nèi)存屏障同樣不會(huì)讓屏障后面的操作跨越到前面去
?
所以只要 y =2 寫操作發(fā)生在 a = y讀操作之前,那么最后 x = 1 肯定先于 b=x,所以 b = 1
?
參考資料:
happens-before俗解
總結(jié)
以上是生活随笔為你收集整理的聊聊高并发(三十六)Java内存模型那些事(四)理解Happens-before规则的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 聊聊高并发(三十五)Java内存模型那些
- 下一篇: Spark中RDD转换成DataFram