volatile的三個(gè)特點(diǎn)保證線程之間的可見性 禁止指令重排 不保證原子性
可見性概念可見性是多線程場景中才討論的,它表示多線程環(huán)境中,當(dāng)一個(gè)線程修改了共享變量的值,其他線程能夠知道這個(gè)修改。
為什么需要可見性緩存一致性問題: public class Test {
public static void main(String[] args) {
Mythread mythread = new Mythread();
new Thread(() -> {
try {
//延時(shí)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,程序運(yùn)行結(jié)果如下 
加上volatile關(guān)鍵字后,程序運(yùn)行結(jié)果如下 
解決方向: 總線鎖: 一次只有一個(gè)線程能通過總線進(jìn)行通信。(效率低,已棄用) MESI緩存一致性協(xié)議,CPU總線嗅探機(jī)制(監(jiān)聽機(jī)制) 有volatile修飾的共享變量在編譯器編譯后進(jìn)行讀寫操作時(shí),指令會多一個(gè)lock前綴,Lock前綴的指令在多核處理器下會引發(fā)兩件事情。
(參考下面兩位大佬的博客) https://blog.csdn.net/jinjiniao1/article/details/100540277 https://blog.csdn.net/qq_33522040/article/details/95319946 每個(gè)處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己緩存的值是不是過期了,當(dāng)處理器發(fā)現(xiàn)自己緩存行對應(yīng)的內(nèi)存地址被修改,就會將當(dāng)前處理器的緩存行設(shè)置為無效狀態(tài), 當(dāng)處理器對這個(gè)數(shù)據(jù)進(jìn)行修改操作的時(shí)候,會重新從系統(tǒng)內(nèi)存中吧數(shù)據(jù)讀到處理器緩存行里。 處理器使用嗅探技術(shù)保證它的內(nèi)部緩存,系統(tǒng)內(nèi)存和其他處理器的緩存在總線上保持一致
禁止指令重排指令重排概念編譯器和CPU在保證最終結(jié)果不變的情況下,對指令的執(zhí)行順序進(jìn)行重排序。
指令重排的問題可以與雙重檢驗(yàn)實(shí)現(xiàn)單例模式聯(lián)系起來看: 首先,一個(gè)對象的創(chuàng)建過程可大致分為以下三步: 分配內(nèi)存空間 執(zhí)行對象構(gòu)造方法,初始化對象 引用指向?qū)嵗龑ο笤诙阎械牡刂?/p>
但是在實(shí)際執(zhí)行過程中,CPU可能會對上述步驟進(jìn)行優(yōu)化,進(jìn)行指令重排 序1->3->2,從而導(dǎo)致引用指向了未初始化的對象,如果這個(gè)時(shí)候另外一個(gè)線 程引用了該未初始化的對象(只執(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;
}
} 上述代碼的運(yùn)行結(jié)果如下圖 
可以看到,循環(huán)執(zhí)行了6666次,但最后的結(jié)果為6663,說明在程序運(yùn)行過程中出 現(xiàn)了重復(fù)的情況。 解決方案使用JUC中的Atomic 類(之后會專門寫一篇學(xué)習(xí)筆記進(jìn)行闡述) 使用synchronized關(guān)鍵字修飾(不推薦)
volatile保證可見性和解決指令重排的底層原理內(nèi)存屏障(內(nèi)存柵欄)組成內(nèi)存屏障分為兩種:Load Barrier 讀屏障 和 Store Barrier 寫屏障 4種類型屏障種類 | 例子 | 作用 |
---|
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í)行順序 在每個(gè)volatile修飾的全局變量讀操作前插入LoadLoad屏障,在讀操作后插入LoadStore屏障
保證某些變量的內(nèi)存可見性 在每個(gè)volatile修飾的全局變量寫操作前插入StoreStore屏障,在寫操作后插入StoreLoad屏障
|