命名
volatile
英文單詞的意思為“揮發(fā)性的,不穩(wěn)定的”如暖。
修飾變量后,說(shuō)明該變量是不穩(wěn)定的忌堂,對(duì)該變量的讀寫需要與主內(nèi)存交互盒至。即該變量在線程的工作內(nèi)存中揮發(fā)了,需要從主內(nèi)存中進(jìn)行讀取浸船。進(jìn)而保證的變量的可見(jiàn)性妄迁。
變量可見(jiàn)性問(wèn)題
在一個(gè)操作非volatile
變量的多線程程序里,每個(gè)線程可能會(huì)從主內(nèi)存復(fù)制變量到CPU緩存以提高性能李命。如果計(jì)算機(jī)包含多個(gè)CPU登淘,每個(gè)線程可能運(yùn)行在不同CPU上。也就是說(shuō)封字,每個(gè)線程可能會(huì)復(fù)制變量到不同的CPU緩存中黔州。如下圖:
使用非volatile
變量將不保證JVM什么時(shí)候會(huì)從主存讀變量到CPU緩存,或什么時(shí)候會(huì)將變量從CPU緩存寫到主存阔籽。
想象一下流妻,多個(gè)線程讀取同一個(gè)共享變量
public class SharedObject {
public int counter = 0;
}
只有線程T1更新counter
變量,但線程T1, T2都可能讀取counter
變量笆制。如果counter
變量未聲明為volatile
绅这,將不能保證counter
變量何時(shí)寫回到主存中,也就不能保證其他線程讀取的counter
變量值的正確性在辆。如圖:
volatile 的可見(jiàn)性保證
可見(jiàn)性保證如下:
- 如果線程A對(duì)一個(gè)
volatile
變量進(jìn)行寫证薇,隨后線程B讀取相同的volatile
變量度苔,那么在寫volatile
變量前的所有對(duì)線程A可見(jiàn)的變量,在線程B讀取該volatile
變量后浑度,對(duì)線程B也是可見(jiàn)的寇窑。 - 如果線程A讀取
volatile
變量,那么所有對(duì)線程A可見(jiàn)的變量將會(huì)從主內(nèi)存重新讀取箩张。
例子:
public class MyClass {
private int years;
private int months
private volatile int days;
public void update(int years, int months, int days){
this.years = years;
this.months = months;
this.days = days;
}
}
update() 方法寫3個(gè)變量甩骏,其中days
聲明為volatile
。
volatile
可見(jiàn)性保證:當(dāng)對(duì)days
進(jìn)行寫時(shí)先慷,所有對(duì)該線程可見(jiàn)的變量(years
, months
)同樣會(huì)被寫進(jìn)主內(nèi)存饮笛。
同時(shí),當(dāng)讀取變量years
, months
, days
時(shí)
public int totalDays() {
int total = this.days;
total += months * 30;
total += years * 365;
return total;
}
當(dāng)totalDays()方法讀取days變量论熙,months
缎浇,years
同樣也是從主內(nèi)存中讀取的。
指令重排的挑戰(zhàn)
因性能原因赴肚,Java VM與CPU允許以程序的指令進(jìn)行重排,只要語(yǔ)義不變二蓝。比如:
int a = 1;
int b = 2;
a++;
b++;
以上指令可能被重排誉券,但語(yǔ)義并沒(méi)有發(fā)生變化:
int a = 1;
a++;
int b = 2;
b++;
然而,指令重排給volatile
變量帶來(lái)挑戰(zhàn)刊愚。讓我們重新看看這段代碼:
public class MyClass {
private int years;
private int months
private volatile int days;
public void update(int years, int months, int days){
this.years = years;
this.months = months;
this.days = days;
}
}
當(dāng)對(duì)days進(jìn)行寫時(shí)踊跟,years
, months
同樣會(huì)被寫進(jìn)主內(nèi)存。但是鸥诽,如果JVM對(duì)指令進(jìn)行了重排商玫,如:
public void update(int years, int months, int days){
this.days = days;
this.months = months;
this.years = years;
}
years
, months
在被賦予新值前就被寫進(jìn)主內(nèi)存,其他線程將不能讀取新值牡借。這時(shí)的指令重排就改變了語(yǔ)義拳昌。
volatile的Happens-before保證
為了解決指令重排的問(wèn)題,除了可見(jiàn)性保證外钠龙,Java volatile
關(guān)鍵字還提供的happens-before保證炬藤。
- 如果原先對(duì)其他變量的讀寫發(fā)生在寫
volatile
變量前,那么對(duì)其他變量的讀寫指令不能重排到寫volatile
變量后碴里; - 如果原先對(duì)其他變量的讀發(fā)生在讀
volatile
變量后沈矿,那么對(duì)其他變量的讀指令不能重排到讀volatile
變量前;
volatile 并不總是滿足的
即使volatile
關(guān)鍵字保證所有對(duì)volatile
變量的讀都直接從主內(nèi)存讀取咬腋,所有對(duì)volatile
變量的寫都直接寫主內(nèi)存羹膳,仍然有定義一個(gè)變量為volatile
不能夠滿足的情況。
在先前的例子中根竿,只有線程1對(duì)共享變量counter
進(jìn)行定陵像,對(duì)counter
定義為volatile
變量能夠保證線程2問(wèn)題能看見(jiàn)最新的值就珠。
實(shí)際上,多線程甚至能寫一個(gè)共享的volatile
變量而主內(nèi)存的值仍然是正確蠢壹,如果其新值不依賴于它先前的值嗓违。也就是說(shuō),如果線程不需要讀取變量先前的值去計(jì)算下一個(gè)值图贸。
只要一個(gè)線程需要先讀取volatile
變量的值蹂季,然后基于該值生成一個(gè)新值,再寫回volatile
變量疏日,那么volatile
變量將不再足夠去保證可見(jiàn)性的正確偿洁。在讀寫volatile
變量的時(shí)間間隙,產(chǎn)生了競(jìng)態(tài)條件 沟优,多個(gè)線程可能讀取相同的volatile
變量的值涕滋,并生成新值,當(dāng)寫入主內(nèi)存時(shí)相互覆蓋彼此的值挠阁。
如:
volatile 何時(shí)是足夠的
前面提到宾肺,兩個(gè)線程同時(shí)讀寫一個(gè)共享變量,僅僅使用volatile
關(guān)鍵字是不夠的侵俗。你需要使用synchronized
保證讀寫變量的原子性锨用。讀寫volatile
變量并不會(huì)阻塞線程的讀寫。因?yàn)檫@個(gè)原因隘谣,必須在臨界區(qū)使用synchronized
增拥。
volatile 的性能考量
- 對(duì)
volatile
變量的讀寫引起主內(nèi)存的讀寫 - 讀寫主內(nèi)存比訪問(wèn)CPU緩存開(kāi)銷更大
- 訪問(wèn)
volatile
變量會(huì)阻止指令重排,而指令重排是正常的性能提升技術(shù)
綜上所述寻歧,只有當(dāng)你真的需要增強(qiáng)變量的可見(jiàn)性時(shí)掌栅,才使用volatile
變量。