保證線程之間的可見性
禁止指令重排
不保證原子性
可見性是多線程場景中才討論的,它表示多線程環(huán)境中,當(dāng)一個線程修改了共享變量的值,其他線程能夠知道這個修改。
緩存一致性問題:
public class Test { public static void main(String[] args) { Mythread mythread = new Mythread(); new Thread(() -> { try { //延時2s,確保進(jìn)入while循環(huán) TimeUnit.SECONDS.sleep(2); //num自增 mythread.increment(); System.out.println("Thread-" + Thread.currentThread().getName() + " current num value:" + mythread.num); } catch (Exception e) { e.printStackTrace(); } }, "test").start(); while(mythread.num == 0){ //dead } System.out.println("game over!!!"); }}class Mythread{ //不加volatile,主線程無法得知num的值發(fā)生了改變,從而陷入死循環(huán) volatile int num = 0; public void increment(){ ++num; }}如上述代碼,如果不加volatile,程序運行結(jié)果如下
加上volatile關(guān)鍵字后,程序運行結(jié)果如下
解決方向:
總線鎖:
一次只有一個線程能通過總線進(jìn)行通信。(效率低,已棄用)
MESI緩存一致性協(xié)議,CPU總線嗅探機(jī)制(監(jiān)聽機(jī)制)
有volatile修飾的共享變量在編譯器編譯后進(jìn)行讀寫操作時,指令會多一個lock前綴,Lock前綴的指令在多核處理器下會引發(fā)兩件事情。
(參考下面兩位大佬的博客)
每個處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己緩存的值是不是過期了,當(dāng)處理器發(fā)現(xiàn)自己緩存行對應(yīng)的內(nèi)存地址被修改,就會將當(dāng)前處理器的緩存行設(shè)置為無效狀態(tài), 當(dāng)處理器對這個數(shù)據(jù)進(jìn)行修改操作的時候,會重新從系統(tǒng)內(nèi)存中吧數(shù)據(jù)讀到處理器緩存行里。
處理器使用嗅探技術(shù)保證它的內(nèi)部緩存,系統(tǒng)內(nèi)存和其他處理器的緩存在總線上保持一致
寫一個volatile變量時,JMM(java共享內(nèi)存模型)會把該線程對應(yīng)的本地內(nèi)存中的共享變量值刷新到主內(nèi)存;
當(dāng)讀一個volatile變量時,JMM會把該線程對應(yīng)的本地內(nèi)存置為無效,線程接下來從主內(nèi)存中讀取共享變量。
編譯器和CPU在保證最終結(jié)果不變的情況下,對指令的執(zhí)行順序進(jìn)行重排序。
可以與雙重檢驗實現(xiàn)單例模式聯(lián)系起來看:
首先,一個對象的創(chuàng)建過程可大致分為以下三步:
分配內(nèi)存空間
執(zhí)行對象構(gòu)造方法,初始化對象
引用指向?qū)嵗龑ο笤诙阎械牡刂?/p>
但是在實際執(zhí)行過程中,CPU可能會對上述步驟進(jìn)行優(yōu)化,進(jìn)行指令重排
序1->3->2,從而導(dǎo)致引用指向了未初始化的對象,如果這個時候另外一個線
程引用了該未初始化的對象(只執(zhí)行了1->3兩步),就會產(chǎn)生異常。
public class Test { public static void main(String[] args) { Mythread mythread = new Mythread(); for(int i = 0; i < 6666; ++i){ new Thread(() -> { try { mythread.increment(); } catch (Exception e) { e.printStackTrace(); } }, "test").start(); } System.out.println("Thread-" + Thread.currentThread().getName() + " current num value:" + mythread.num); }}class Mythread{ volatile int num = 0; public void increment(){ ++num; }}
上述代碼的運行結(jié)果如下圖
可以看到,循環(huán)執(zhí)行了6666次,但最后的結(jié)果為6663,說明在程序運行過程中出
現(xiàn)了重復(fù)的情況。
使用JUC中的Atomic
類(之后會專門寫一篇學(xué)習(xí)筆記進(jìn)行闡述)
使用synchronized關(guān)鍵字修飾(不推薦)
內(nèi)存屏障分為兩種:Load Barrier 讀屏障 和 Store Barrier 寫屏障
種類 | 例子 | 作用 |
---|---|---|
LoadLoad屏障 | Load1; LoadLoad; Load2 | 保證Load1讀取操作讀取完畢后再去執(zhí)行Load2后續(xù)讀取操作 |
LoadStore屏障 | Load1; LoadStore; Store2 | 保證Load1讀取操作讀取完畢后再去執(zhí)行Load2后續(xù)寫入操作 |
StoreStore屏障 | Store1; StoreStore; Store2 | 保證Load1的寫入對所有處理器可見后再去執(zhí)行Load2后續(xù)寫入操作 |
StoreLoad屏障 | Store1; StoreLoad; Load2 | 保證Load1的寫入對所有處理器可見后再去執(zhí)行Load2后續(xù)讀取操作 |
保證特定操作的執(zhí)行順序
在每個volatile修飾的全局變量讀操作前插入LoadLoad屏障,在讀操作后插入LoadStore屏障
保證某些變量的內(nèi)存可見性
在每個volatile修飾的全局變量寫操作前插入StoreStore屏障,在寫操作后插入StoreLoad屏障