voliate關鍵字的兩個作用
1、 保證變量的可見性:當一個被volatile關鍵字修飾的變量被一個線程修改的時候毙石,其他線程可以立刻得到修改之后的結果蛋勺。當一個線程向被volatile關鍵字修飾的變量寫入數(shù)據(jù)的時候凹髓,虛擬機會強制它被值刷新到主內(nèi)存中。當一個線程用到被volatile關鍵字修飾的值的時候奶陈,虛擬機會強制要求它從主內(nèi)存中讀取易阳。
2、 屏蔽指令重排序:指令重排序是編譯器和處理器為了高效對程序進行優(yōu)化的手段尿瞭,它只能保證程序執(zhí)行的結果時正確的闽烙,但是無法保證程序的操作順序與代碼順序一致。這在單線程中不會構成問題声搁,但是在多線程中就會出現(xiàn)問題黑竞。非常經(jīng)典的例子是在單例方法中同時對字段加入voliate,就是為了防止指令重排序疏旨。
編譯期重排序的典型就是通過調(diào)整指令順序很魂,做到在不改變程序語義的前提下,盡可能減少寄存器的讀取檐涝、存儲次數(shù)遏匆,充分復用寄存器的存儲值
。
比如我們有如下代碼:
int x = 10;
int y = 9;
x = x+10;
假設編譯器直接對上面代碼進行編譯谁榜,不進行重排序的話幅聘,我們簡單分析一下執(zhí)行這段代碼的過程,首先加載x變量的內(nèi)存地址到地址寄存器窃植,然后會加載10到數(shù)據(jù)寄存器帝蒿,然后CPU通過mov指令把10寫入到地址寄存器中指定的內(nèi)存地址中。然后加載y變量的內(nèi)存地址到地址寄存器巷怜,加載9到數(shù)據(jù)寄存器葛超,把9寫入到內(nèi)存地址中暴氏。進行第三行執(zhí)行時,我們發(fā)現(xiàn)CPU需要重新加載x的內(nèi)存地址和數(shù)據(jù)到寄存器绣张,但如果我把第三行和第二行換一下順序答渔,那么執(zhí)行過程中對于寄存器的存取就可以少很多次,同時對于程序結果沒有任何影響侥涵。
另一個例子可以看下面的雙重檢查鎖構造單例的代碼
public class Singleton {
private volatile static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) { // 1
synchronized(Singleton.class) {
if (singleton == null) {
singleton = new Singleton(); // 2
}
}
}
return singleton;
}
}
實際上當程序執(zhí)行到2處的時候沼撕,如果我們沒有使用volatile關鍵字修飾變量singleton,就可能會造成錯誤独令。這是因為使用new關鍵字初始化一個對象的過程并不是一個原子的操作端朵,它分成下面三個步驟進行:
a. 給 singleton 分配內(nèi)存
b. 調(diào)用 Singleton 的構造函數(shù)來初始化成員變量
c. 將 singleton 對象指向分配的內(nèi)存空間(執(zhí)行完這步 singleton 就為非 null 了)
如果虛擬機存在指令重排序優(yōu)化,則步驟b和c的順序是無法確定的燃箭。如果A線程率先進入同步代碼塊并先執(zhí)行了c而沒有執(zhí)行b,此時因為singleton已經(jīng)非null舍败。這時候線程B到了1處招狸,判斷singleton非null并將其返回使用,因為此時Singleton實際上還未初始化邻薯,自然就會出錯裙戏。synchronized可以解決內(nèi)存可見性,但是不能解決重排序問題厕诡。
但是特別注意在jdk 1.5以前的版本使用了volatile的雙檢鎖還是有問題的累榜。其原因是Java 5以前的JMM(Java 內(nèi)存模型)是存在缺陷的,即時將變量聲明成volatile也不能完全避免重排序灵嫌,主要是volatile變量前后的代碼仍然存在重排序問題壹罚。這個volatile屏蔽重排序的問題在jdk 1.5 (JSR-133)中才得以修復,這時候jdk對volatile增強了語義寿羞,對volatile對象都會加入讀寫的內(nèi)存屏障猖凛,以此來保證可見性,這時候2-3就變成了代碼序而不會被CPU重排绪穆,所以在這之后才可以放心使用volatile辨泳。