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