java中volatile聲明變量,有兩個(gè)作用
- 保證變量對所有線程的可見性
- 禁止指令重排
保證可見性
多線程訪問共享變量時(shí),聲明volatile可以保證了共享變量可見性÷腹可見性,即當(dāng)一個(gè)線程修改共享變量時(shí)哪轿,另一個(gè)線程能讀到變量修改后的值盈魁。
沒有了解過計(jì)算機(jī)系統(tǒng)低層實(shí)現(xiàn)的同學(xué),可能會好奇窃诉,為什么會發(fā)生一個(gè)線程更新一個(gè)變量杨耙,另一線程讀到的是是更新前的值的情況赤套?
計(jì)算機(jī)系統(tǒng),為了提高處理速度按脚,CPU其實(shí)并不直接與內(nèi)存進(jìn)行通信于毙,而是將內(nèi)存中的數(shù)據(jù)讀取到CPU高速緩存(L1,L2辅搬,L3)中唯沮,操作完后也并不總是會馬上寫回內(nèi)存。只有當(dāng)緩存被置換出去的時(shí)候堪遂,才會去寫內(nèi)存介蛉。
多處理器環(huán)境下,保證緩存一致性溶褪,有兩種方式:總線鎖和MESI緩存一致性協(xié)議币旧。
總線鎖(bus locking):
當(dāng)一個(gè)CPU對緩存中的數(shù)據(jù)進(jìn)行操作的時(shí)候,會往總線中發(fā)送一個(gè)Lock信號猿妈。這個(gè)時(shí)候吹菱,所有CPU收到這個(gè)信號之后就不操作自己緩存中的對應(yīng)數(shù)據(jù)了,當(dāng)操作結(jié)束彭则,釋放鎖以后鳍刷,所有的CPU就去內(nèi)存中獲取最新數(shù)據(jù)更新。
總線鎖帶來的性能成本高昂俯抖,更多的計(jì)算機(jī)采用的是MESI緩存一致性協(xié)議输瓜。
MESI協(xié)議, 會在緩存中存儲一個(gè)標(biāo)志位芬萍。這個(gè)標(biāo)志位有以下四種狀態(tài):
- M: Modify尤揣,修改緩存,當(dāng)前CPU的緩存已經(jīng)被修改了柬祠,即與內(nèi)存中數(shù)據(jù)已經(jīng)不一致了
- E: Exclusive北戏,獨(dú)占緩存,當(dāng)前CPU的緩存和內(nèi)存中數(shù)據(jù)保持一致瓶盛,而且其他處理器并沒有可使用的緩存數(shù)據(jù)
- S: Share最欠,共享緩存,和內(nèi)存保持一致的一份拷貝惩猫,多組緩存可以同時(shí)擁有針對同一內(nèi)存地址的共享緩存段
- I: Invalid,失效緩存蚜点,這個(gè)說明CPU中的緩存已經(jīng)不能使用了
每個(gè)CPU都會監(jiān)聽其他CPU對緩存的讀寫操作轧房。
- 當(dāng)監(jiān)聽到其他CPU有讀取操作,而自己的緩存處于M或E時(shí)绍绘,就將自己的緩存寫入內(nèi)存奶镶,并將自己狀態(tài)修改為S迟赃。 - 當(dāng)監(jiān)聽到其他CPU有寫入內(nèi)存操作,且緩存為S狀態(tài)厂镇,則將狀態(tài)職位I纤壁。
- 當(dāng)CPU的緩存狀態(tài)是I,CPU將從內(nèi)存中讀取捺信。
使用volatile聲明的變量酌媒,JVM會發(fā)送lock指令,將緩存寫回內(nèi)存迄靠。由于緩存一致性協(xié)議秒咨,寫回緩存的時(shí)候,其他處理器的緩存將失效掌挚。從而保證了該變量值得一致性雨席。
禁止指令重排
編譯器和處理器為了提升程序性能,會按照一定的規(guī)則進(jìn)行對指令進(jìn)行重排順序吠式。在單線程下陡厘,這樣的重排并不會帶來問題。因?yàn)橹嘏艜袷豠s-if-serial語義.
為了遵守as-if-serial語義特占,編譯器和處理器不會對存在數(shù)據(jù)依賴關(guān)系的操作做重排序糙置,因?yàn)檫@種重排序會改變執(zhí)行結(jié)果。但是摩钙,如果操作之間不存在數(shù)據(jù)依賴關(guān)系罢低,這些操作可能被編譯器和處理器重排序。例如:
double pi = 3.14; //A
double r = 1.0; //B
double area = pi * r * r; //C
C 依賴于A 和B胖笛,因此重排時(shí)C不會被重排到A和B之前网持。而A和B沒有依賴,重排也復(fù)合as-if-serial語義长踊,編譯器和處理器可以對其進(jìn)行重排功舀。
經(jīng)常提到的使用雙檢查鎖來創(chuàng)建singleton對象的問題,正是由于指令重排導(dǎo)致的身弊。
public class Singleton {
private static Singleton instance = null;
private Singleton() { }
public static Singleton getInstance() {
if(instance == null) {
synchronzied(Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
new Singleton()并非是一個(gè)原子操作辟汰,它有多條指令組成:
memory = allocate(); //1:分配對象的內(nèi)存空間
ctorInstance(memory); //2:初始化對象
instance = memory; //3:設(shè)置instance指向剛分配的內(nèi)存地址
但是經(jīng)過重排序后:
memory = allocate(); //1:分配對象的內(nèi)存空間
instance = memory; //3:設(shè)置instance指向剛分配的內(nèi)存地址,此時(shí)對象還沒被初始化
ctorInstance(memory); //2:初始化對象
指令重排之后阱佛,instance指向分配好的內(nèi)存放在了前面帖汞,而這段內(nèi)存的初始化被排在了后面,在線程A初始化完成這段內(nèi)存之前凑术,線程B雖然進(jìn)不去同步代碼塊翩蘸,但是在同步代碼塊之前的判斷就會發(fā)現(xiàn)instance不為空,此時(shí)線程B獲得instance對象進(jìn)行使用就可能發(fā)生錯(cuò)誤淮逊。
使用volatile會插入內(nèi)存屏障指令催首,防止編譯和運(yùn)行時(shí)指令被重排扶踊。