1. JMM規定CPU執行的(線程執行的)一些交互操作(應該并不是指令名稱,只是抽象動作概念):
每條指令都是原子的(指令內部的操作們粘在一起的,不可分開的,要么都執行要么都不執行)
(JMM規定每條指令都是原子的,但是對double和long的操作除外)
lock:作用于主內存的變量, 將該變量被唯一的線程獨占。對于一個變量而言,會變成單核CPU。
unlock:作用于主內存的變量,與lock相反。
read:作用于主內存的變量,將變量的值拿在手里。
load:作用于工作內存的變量,將read操作拿著的變量的值,放入工作內存的變量的拷貝空間中。
store:作用于工作內存的變量,將變量的值拿在手里。
write:作用于主內存的變量。將store操作操作拿著的變量的值,放入主內存中。
use:作用于工作內存的變量,將變量提供給CPU。
assign:作用于工作內存的變量,接收CPU的指令,將一個值賦值給變量。
JMM對以上指令的規定:
1. read-load和store-write,必須成對出現,且每對內部按順序執行。(但是可以不連續執行)
2. 如果一個線程執行了assign,則后面必須store-write。即 assign+(store-write)
3. 如果一個線程沒執行assign,則不允許store-write。即 no assign,no(store-write)
4. 一個線程使用變量(執行use或者store-write)之前,這個變量必須load,(如果load之后變量為null的話還需要assign初始化)
5. 一個變量只能被同一個線程lock,但是可以lock多次。記得unlock時也要unlock那么多次。
6. 一個線程執行lock變量,會清空這個變量的在工作內存的值,所以需要lock之后執行load和assign。
7. 一個線程只能unlock當前線程已經lock了的變量。
8. 一個線程執行unlock,之前必須store-write同步會主內存。unlock ‘before = (store-write)
JMM允許對double和long的操作可以不需要滿足原子性:
因為double和long是64位,又因為32位CPU的緩存行是32位,所以有時不可能實現原子性。
所以JMM允許double和long的操作可以不需要滿足原子性。
但是現在的商用JVM都已經把double和long的操作實現為原子了。
2. Volatile
2.1 Volatile語義1:(可見性語義)
Volatile變量改變時會直接寫入主內存。但是只保證可見性,并不保證原子性。
意思是,只保證數據顯示的值是最新的,但是不保證我用這條數據的時候別人不用,如果大家都用,可能會造成某些人的操作被覆蓋而看不出來。
例子:以下場景不適合用Volatile:
新建20個線程,對同一個Volatile變量1萬次++,但是結果小于20萬。
解決方式:加鎖 或者 CAS的AtomInterger
2.2 Volatile語義1的推論:
滿足以下兩個條件時,適合用Volatile。如果不滿足的話還是用Synchronized或者JUC吧:
1. 運算結果并不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值。(場景比如:不并發進行++)
2. 變量不需要與其他的狀態變量共同參與不變約束。(場景比如:用Volatile變量作為開關(且只用這一個Volatile變量),控制某個方法是否執行)
2.3 Volatile語義2:(有序性語義)
禁止指令重排序。
例子:以下場景適合用Volatile:
Volatile+DCL單例模式。
2.4 Volatile的實現方式為LOCK指令,相當于內存屏障:
當變量為Volatile,在改變Volatile變量的值以后,比普通變量時多出一條匯編代碼 ,注意是匯編代碼而不是字節碼。
lock addl $0x0,(%esp)
這句話的關鍵是lock,
加入了lock前綴之后就變成了如下操作:
1. 對CPU總線加鎖(不過后來的處理器都采用鎖緩存替代鎖總線,因為鎖總線的開銷比較大)
2. 這時其他CPU的讀寫請求都會被阻塞,直到鎖釋放。
3. 當前CPU改變當前高速緩存中的Volatile變量的值,強制做了一次store+write操作,寫入主內存。
4. 鎖釋放,同時清空其他CPU的相應的緩存行(也有人說是設置為無效Invalid)。
5. 當其他CPU讀取那個Volatile變量時,發現空行或者無效,那怎么辦呢?根據MESI,會從主存獲取。
使語義1生效的原因:
即操作12345
使語義2生效的原因:
即操作12345,其實整個操作相當于在寫Volatile變量加了全屏障。
ps.
Volatile只可以修飾成員變量(靜態不靜態都可以)但是不可以修飾局部變量(idea不允許)。因為局部變量只存活于方法棧中(工作內存中)所以不存在主存可見性的問題。
加了Volatile的成員變量的字節碼的區別是:變量多了一個ACC_VOLATILE的flag而已。
public class test { static int i; // static Volatile int i; void hy () { i = 20; } }

3. 內存屏障
作用:
1. 保證數據的可見性
2. 防止指令之間的重排序
x86的內存屏障分為三類及其作用:
內存屏障其實是Intel提供的硬件指令:sfence (Store Barrier)、lfence(Load Barrier)、mfence (Full Barrier)
Intel還提供了一個lock指令前綴,這個前綴是專門用于加在指令(比如add)之前的,表示當前指令操作的內存只能由當前CPU使用,而且自帶Full Barrior效果;(就是Volatile的實現)
1. 讀屏障Load Barrier(lfence + 內存地址):先使緩存行失效,然后觸發強制從主存獲取數據的動作。
保證的是,Load Barrier之前的load和之后的load不會被重排序。
2. 寫屏障Store Barrier(sfence + 內存地址):觸發強制寫入主存的動作,對其他CPU可見。
保證的是,Store Barrier之前的store和之后的store不會被重排序。
3. 全屏障 Full Barrier(mfence + 內存地址):強制從主存讀取以及強制向主存寫。
保證的是,Full Barrier上面的不能下去,同時,Full Barrier下面的不能上去。
(全屏障是一個原子效果,并不是等于寫屏障+讀屏障,因為寫屏障+讀屏障這個組合也可能出現重排(因為x86允許Store-Load 重排序))
X86下僅支持一種指令重排:Store-Load ,
即讀操作可能會重排到寫操作前面,同時不同線程的寫操作并沒有保證全局可見,例子見《Intel? 64 and IA-32 Architectures Software Developer’s Manual》手冊8.6.1、8.2.3.7節。
要注意的是這個問題只能用mfence(或者是Lock前綴)解決,不能靠組合sfence和lfence解決。
(用sfence+lfence組合僅可以解決重排問題,但不能解決全局可見性問題,簡單理解不如視為sfence和lfence本身也能亂序重拍)
JMM的內存屏障分為四類:
Java編譯器會在適當位置插入以下內存屏障來禁止重排序
1. LoadLoad Barrier:相當于x86的 Load Barrier
2. LoadStore Barrier:相當于x86的讀屏障+寫屏障的原子組合。
3. StoreLoad Barrier:相當于x86的 Full Barrier(即寫屏障+讀屏障的原子組合)
4. StoreStore Barrier:相當于x86的 Store Barrier

4. Final
如果final修飾一個基本數據類型,表示該基本數據類型的值一旦在初始化后便不能發生變化;
如果final修飾一個引用類型,則在對其初始化之后便不能再讓其指向其他對象了,但該引用所指向的對象的內容是可以發生變化的。
(其實本質上是一回事,因為引用的值是一個地址,final要求值,即地址的值不發生變化。)
final修飾的屬性必須要初始化后才能返回這個類的實例對象,如果不初始化賦值會編譯失敗。可以在變量聲明的時候初始化 + 也可以在構造函數中對這個變量賦初值。
Final的實現內存屏障:
1. 對某個類的final域的初始化后,加入一個Store-load屏障,然后才能使用這個對象。(如果不加屏障就可能會重排序了,就會可能拿到的對象的final域是沒有初始化的)(盲猜也是Lock前綴,全屏障)
2. (沒明白,可以不用說)初次讀這個對象的final域之前,加入一個load-Store屏障。(如果不加屏障就可能會重排序了,就會可能拿到的對象的final域是沒有初始化的)
題外話,需要注意Final的使用方法:
Final的成員變量的注意事項:
1.
該成員變量必須在創建對象之前進行賦值,否則編譯失敗。
即定義成員變量的時候手動賦值 或者 利用構造器對成員變量進行賦值
2.
final變量是屬于對象的,意思是同一個類的兩個對象的各自final變量可以是不相同的。
5. 硬件層面需要解決兩個問題之一:緩存一致性問題:(CPU之間橫向層面,多核操作同一變量的問題)
問題產生原因:CPU1緩存與CPU2緩存之間數據不同步的問題。
解決方式1(不推薦):通過在總線加LOCK#鎖的方式(因為會變成單核CPU,不推薦)
解決方式2(采用):MESI(緩存一致性協議)+Store Buffer(緩沖區)+Invalidate Queue
MESI協議:
Store Buffer & Invalidate Queue為MESI 提供了異步解決方案,強調的是異步。
Store Buffer:讀寫時的更高級緩存
Invalidate Queue:而當一個CPU核收到Invalid消息時,會把消息寫入自身的Invalidate Queue中,隨后異步將其設為Invalid狀態(具體什么時候未知)。
Invalid狀態:非法
Share狀態 :正在被共享
Exclusive狀態:正在被獨占
Modified狀態:緩存行已被修改,但是還未寫入主存。
CPU向緩存寫數據時,
先寫入Store-Buffer,
如果該緩存行是Invalid,則從主存中獲取并刷新緩存先,再把自己緩存行設置為Share。
如果該緩存行是Share ,就把其他CPU的這條緩存行都設置為Invalid,然后自己緩存行變為Exclusive。
如果該緩存行是Exclusive,就把該緩存行設置為Modified,寫入緩存行,然后異步寫入主存(具體什么時候未知),然后把自己緩存行設置為Exclusive。
如果該緩存行是Modified,就先等待異步寫入主存后(具體什么時候未知),再變為Exclusive先。
CPU從緩存讀數據時,
先掃描Store-Buffer,如果沒找到就去找緩存,
如果該緩存行是Invalid,則從主存中獲取并刷新緩存,變為Share 。
如果該緩存行是Share ,則直接讀。
如果該緩存行是Exclusive ,則直接讀。
如果該緩存行是Modified,需要等待變成Exclusive才可以讀。
缺點1:當一個CPU核收到Invalid消息時,會把消息寫入自身的Invalidate Queue中,隨后異步將其設為Invalid狀態(具體什么時候未知)。這個期間可能就會發生讀取數據的操作,而此時的數據是臟數據。
缺點2:當CPU向緩存寫數據時,在Modified之后&Exclusive之前,異步寫入主存(具體什么時候未知),而此時別人可能從主存讀取了臟數據。
6. 硬件層面需要解決兩個問題之二: 指令重排問題:(時間縱向層面,多核操作同一變量的問題)
問題原因之編譯器重排:
由于編譯器只需要滿足JMM的as-if-serial規則,
即,在單線程下不能改變結果。
所以默認允許了指令重排序。
但是會導致多線程下出現問題。
問題原因之處理器重排:
為了優化CPU運算效率,CPU在保證運算結果不變的情況下,允許指令重排。
X86默認只允許Store-Load形式的指令重排。不允許Load-Load,Load-Store,Store-Load形式的指令重排。
但是仍會導致多線程下出現問題。
解決方式1:Volatile語義2,即內存屏障。(見上文)
應用舉例:Volatile+DCL
解決方式2:Final語義。(見上文)
解決方式3:Synchronized(lock同理)
但是有局限性。
synchronized(lock同理)只是把多線程執行一個塊(lock的trycatch塊)變為對于單線程執行一個塊(lock的trycatch塊)。
synchronized(lock同理)保證的是線程1塊對線程2的同一個塊(lock的trycatch塊)不重排,但是塊內部的邏輯就不能保證不重排了。
解決方式4:java自帶的happenbefore原則
7. 硬件層面需要解決兩個問題之總結:
MESI(CPU之間橫向層面,多核操作同一變量的問題,解決多核操作同一變量的問題)+內存屏障(時間縱向層面,解決多核操作同一變量的問題)組成了x86的解決方式。
其中MESI是必然發生的,而內存屏障是可以我們手動加上的(Volatile)。
Synchronized和Lock是JVM的解決方式
8. JMM層面要求代碼需要滿足三個特性之一:原子性
解決方式1:Synchronized修飾(lock同理)
每一個synchronized塊(lock的trycatch塊),都是一整個原子操作。
解決方式2:CAS
cmpxchg一個指令完成了比較和交換兩個操作。
解決方式3:AQS(內部使用了Volatile+CAS維護State的改變+同步隊列的節點狀態、節點前后、頭尾節點等)
9. JMM層面要求代碼需要滿足三個特性之二:可見性
解決方式1:Volatile
Volatile語義1.(見上文)
解決方式2:Synchronized(lock同理)
但是有局限性。
synchronized(lock同理)只是把多線程執行一個塊(lock的trycatch塊)變為對于單線程執行一個塊(lock的trycatch塊)
synchronized(lock同理)保證的是線程1塊對線程2的同一個塊可見。不能保證塊內的第一行對第二行可見。
解決方式3:final
final語義(見上文)
解決方式4:AQS(內部使用了Volatile+CAS維護State的改變+同步隊列的節點狀態、節點前后、頭尾節點等)
10. JMM層面要求代碼需要滿足三個特性之三:有序性
解決方式1:Volatile語義2,即內存屏障。(見上文)
應用舉例:Volatile+DCL
解決方式2:Final語義。(見上文)
解決方式3:Synchronized(lock同理)
但是有局限性。
synchronized(lock同理)只是把多線程執行一個塊(lock的trycatch塊)變為對于單線程執行一個塊(lock的trycatch塊)。
synchronized(lock同理)保證的是線程1塊對線程2的同一個塊(lock的trycatch塊)不重排,但是塊內部的邏輯就不能保證不重排了。
解決方式4:java自帶的happens-before原則
解決方式5:AQS(內部使用了Volatile+CAS維護State的改變+同步隊列的節點狀態、節點前后、頭尾節點等)
11. Happens-Before規則:
為了實現有序性,我們可以使用Volatile或Synchronized或final,但是這樣對程序員不友好,因為代碼會很煩瑣。為了輔助Volatile或Synchronized或final,所以Java語言自帶了Happens-Before規則:。
Happens-Before規則的意思是,java天生自帶了某些情況下的兩個操作的前后可見,
即假如A happens- before B,則A對于B是可見的。但是并不表示A必須在B之前執行。
意思是,可以重排,但是不能影響我們兩者的有序性,所以其實也一定程度地約束了不允許重排。
至于Java是怎么保證AB有序性的(或者叫A對于B的可見性),盲猜是使用了JMM的四種內存屏障吧。
程序順序規則:在一個線程內,
前:前面的操作,后:后面的操作。
Synchronize規則:對于一個monitor鎖,
前:unlock,后:lock。
Volatile規則:對于一個 volatile變量,
前:寫,后:讀。
線程啟動規則:對于一個Thread對象,
前:Thread.start,后:Thread的內部方法被調用。
線程中斷規則:對于一個Thread對象,
前:Thread.interrupt,后:interrupt被檢測到
線程終止規則:對于一個Thread對象t1,還有一個線程t2在t1內部運行t2.join,
前:t2的所有操作,后:t2.join結束后,t1恢復
對象終結原則:對于一個對象,
前:對象初始化,后:對象執行finalize
傳遞性:對于操作A先于操作B,并且,操作B先于操作C,則操作A先于操作C

浙公網安備 33010602011771號