关键字_Java Volatile关键字
作者| Arun Manivannan
譯者|有孚
編輯|包包 ? ??
Java的volatile關(guān)鍵字用于標(biāo)記一個(gè)變量“應(yīng)當(dāng)存儲(chǔ)在主存”。更確切地說(shuō),每次讀取volatile變量,都應(yīng)該從主存讀取,而不是從CPU緩存讀取。每次寫(xiě)入一個(gè)volatile變量,應(yīng)該寫(xiě)到主存中,而不是僅僅寫(xiě)到CPU緩存。
實(shí)際上,從Java 5開(kāi)始,volatile關(guān)鍵字除了保證volatile變量從主存讀寫(xiě)外,還提供了更多的保障。我將在后面的章節(jié)中進(jìn)行說(shuō)明。
變量可見(jiàn)性問(wèn)題
Java的volatile關(guān)鍵字能保證變量修改后,對(duì)各個(gè)線程是可見(jiàn)的。這個(gè)聽(tīng)起來(lái)有些抽象,下面就詳細(xì)說(shuō)明。
在一個(gè)多線程的應(yīng)用中,線程在操作非volatile變量時(shí),出于性能考慮,每個(gè)線程可能會(huì)將變量從主存拷貝到CPU緩存中。如果你的計(jì)算機(jī)有多個(gè)CPU,每個(gè)線程可能會(huì)在不同的CPU中運(yùn)行。這意味著,每個(gè)線程都有可能會(huì)把變量拷貝到各自CPU的緩存中,如下圖所示:
對(duì)于非volatile變量,JVM并不保證會(huì)從主存中讀取數(shù)據(jù)到CPU緩存,或者將CPU緩存中的數(shù)據(jù)寫(xiě)到主存中。這會(huì)引起一些問(wèn)題,在后面的章節(jié)中,我會(huì)來(lái)解釋這些問(wèn)題。
試想一下,如果有兩個(gè)以上的線程訪問(wèn)一個(gè)共享對(duì)象,這個(gè)共享對(duì)象包含一個(gè)counter變量,下面是代碼示例:
public class SharedObject { public int counter = 0;}如果只有線程1修改了(自增)counter變量,而線程1和線程2兩個(gè)線程都會(huì)在某些時(shí)刻讀取counter變量。
如果counter變量沒(méi)有聲明成volatile,則counter的值不保證會(huì)從CPU緩存寫(xiě)回到主存中。也就是說(shuō),CPU緩存和主存中的counter變量值并不一致,如下圖所示:
這就是“可見(jiàn)性”問(wèn)題,線程看不到變量最新的值,因?yàn)槠渌€程還沒(méi)有將變量值從CPU緩存寫(xiě)回到主存。一個(gè)線程中的修改對(duì)另外的線程是不可見(jiàn)的。
volatile可見(jiàn)性保證
Java的volatile關(guān)鍵字就是設(shè)計(jì)用來(lái)解決變量可見(jiàn)性問(wèn)題。將counter變量聲明為volatile,則在寫(xiě)入counter變量時(shí),也會(huì)同時(shí)將變量值寫(xiě)入到主存中。同樣的,在讀取counter變量值時(shí),也會(huì)直接從主存中讀取。
下面的代碼演示了如果將counter聲明為volatile:
public class SharedObject { public volatile int counter = 0;}將一個(gè)變量聲明為volatile,可以保證變量寫(xiě)入時(shí)對(duì)其他線程的可見(jiàn)。
在上面的場(chǎng)景中,一個(gè)線程(T1)修改了counter,另一個(gè)線程(T2)讀取了counter(但沒(méi)有修改它),將counter變量聲明為volatile,就能保證寫(xiě)入counter變量后,對(duì)T2是可見(jiàn)的。
然而,如果T1和T2都修改了counter的值,只是將counter聲明為volatile還遠(yuǎn)遠(yuǎn)不夠,后面會(huì)有更多的說(shuō)明。
完整的volatile可見(jiàn)性保證
實(shí)際上,volatile的可見(jiàn)性保證并不是只對(duì)于volatile變量本身那么簡(jiǎn)單。可見(jiàn)性保證遵循以下規(guī)則:
如果線程A寫(xiě)入一個(gè)volatile變量,線程B隨后讀取了同樣的volatile變量,則線程A在寫(xiě)入volatile變量之前的所有可見(jiàn)的變量值,在線程B讀取volatile變量后也同樣是可見(jiàn)的。
如果線程A讀取一個(gè)volatile變量,那么線程A中所有可見(jiàn)的變量也會(huì)同樣從主存重新讀取。
下面用一段代碼來(lái)示例說(shuō)明:
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()方法寫(xiě)入3個(gè)變量,其中只有days變量是volatile。
完整的volatile可見(jiàn)性保證意味著,在寫(xiě)入days變量時(shí),線程中所有可見(jiàn)變量也會(huì)寫(xiě)入到主存。也就是說(shuō),寫(xiě)入days變量時(shí),years和months也會(huì)同時(shí)被寫(xiě)入到主存。
下面的代碼讀取了years、months、days變量:
public class MyClass { private int years; private int months private volatile int days; public int totalDays() { int total = this.days; total += months * 30; total += years * 365; return total; } public void update(int years, int months, int days){ this.years = years; this.months = months; this.days = days; }}請(qǐng)注意totalDays()方法開(kāi)始讀取days變量值到total變量。在讀取days變量值時(shí),months和years的值也會(huì)同時(shí)從主存讀取。因此,按上面所示的順序讀取時(shí),可以保證讀取到days、months、years變量的最新值。
'' ? ?譯者注:可以將對(duì)volatile變量的讀寫(xiě)理解為一個(gè)觸發(fā)刷新的操作,寫(xiě)入volatile變量時(shí),線程中的所有變量也都會(huì)觸發(fā)寫(xiě)入主存。而讀取volatile變量時(shí),也同樣會(huì)觸發(fā)線程中所有變量從主存中重新讀取。因此,應(yīng)當(dāng)盡量將volatile的寫(xiě)入操作放在最后,而將volatile的讀取放在最前,這樣就能連帶將其他變量也進(jìn)行刷新。上面的例子中,update()方法對(duì)days的賦值就是放在years、months之后,就是保證years、months也能將最新的值寫(xiě)入到主存,如果是放在兩個(gè)變量之前,則days會(huì)寫(xiě)入主存,而years、months則不會(huì)。反過(guò)來(lái),totalDays()方法則將days的讀取放在最前面,就是為了能同時(shí)觸發(fā)刷新years、months變量值,如果是放后面,則years、months就可能還是從CPU緩存中讀取值,而不是從主存中獲取最新值。 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?'' |
指令重排問(wèn)題
出于性能考慮,JVM和CPU是允許對(duì)程序中的指令進(jìn)行重排的,只要保證(重排后的)指令語(yǔ)義一致即可。如下代碼為例:
int a = 1;int b = 2;a++;b++;這些指令可以按以下順序重排,而不改變程序的語(yǔ)義:
int a = 1;a++;int b = 2;b++;然而,指令重排面臨的一個(gè)問(wèn)題就是對(duì)volatile變量的處理。還是以前面提到的MyClass類(lèi)來(lái)說(shuō)明:
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()變量寫(xiě)了days值,則years、months的最新值也會(huì)寫(xiě)入到主存。但是,如果JVM重排了指令,比如按以下方式重排:
public void update(int years, int months, int days){ this.days = days; this.months = months; this.years = years;}在days被修改時(shí),months、years的值也會(huì)寫(xiě)入到主存,但這時(shí)進(jìn)行寫(xiě)入,months、years并不是新的值(譯者注:即在months、years被賦新值之前,就觸發(fā)了這兩個(gè)變量值寫(xiě)入主存的操作,自然這兩個(gè)變量在主存中的值就不是新值)。新的值自然對(duì)其他線程是不可見(jiàn)的。指令重排導(dǎo)致了程序語(yǔ)義的改變。
Java對(duì)此有一個(gè)解決方法,我們會(huì)在下一章節(jié)中說(shuō)明。
Java volatile Happens-Before保證
為了解決指令重排的問(wèn)題,Java的volatile關(guān)鍵字在可見(jiàn)性之外,又提供了happends-before保證。happens-before原則如下:
如果有讀寫(xiě)操作發(fā)生在寫(xiě)入volatile變量之前,讀寫(xiě)其他變量的指令不能重排到寫(xiě)入volatile變量之后。寫(xiě)入一個(gè)volatile變量之前的讀寫(xiě)操作,對(duì)volatile變量是有happens-before保證的。注意,如果是寫(xiě)入volatile之后,有讀寫(xiě)其他變量的操作,那么這些操作指令是有可能被重排到寫(xiě)入volatile操作指令之前的。但反之則不成立。即可以把位于寫(xiě)入volatile操作指令之后的其他指令移到寫(xiě)入volatile操作指令之前,而不能把位于寫(xiě)入volatile操作指令之前的其他指令移到寫(xiě)入volatile操作指令之后。
如果有讀寫(xiě)操作發(fā)生在讀取volatile變量之后,讀寫(xiě)其他變量的指令不能重排到讀取volatile變量之前。注意,如果是讀取volatile之前,有讀取其他變量的操作,那么這些操作指令是有可能被重排到讀取volatile操作指令之后的。但反之則不成立。即可以把位于讀取volatile操作指令之前的指令移到讀取volatile操作指令之后,而不能把位于讀取volatile操作指令之后的指令移到讀取volatile操作指令之前。
以上的happens-before原則為volatile關(guān)鍵字的可見(jiàn)性提供了強(qiáng)制保證。
'' ? ?譯者注:這兩個(gè)原則讀起來(lái)有些拗口(當(dāng)然翻譯也不足夠好),其實(shí)就是不管JVM怎么去禁止/允許某些情況下的指令重排,最終就是保證“完整的volatile可見(jiàn)性保證”的那種效果,所以,只要理解了“完整的volatile可見(jiàn)性保證”的效果就足夠了 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?? ? ? ? ?? '' |
volatile并不總是可行的
雖然volatile關(guān)鍵字能保證volatile變量的所有讀取都是直接從主存讀取,所有寫(xiě)入都是直接寫(xiě)入到主存中,但在一些情形下,僅僅是將變量聲明為volatile還是遠(yuǎn)遠(yuǎn)不夠的。
就像前面示例所說(shuō)的,線程1寫(xiě)入共享變量counter的值,將counter聲明為volatile已經(jīng)足夠保證線程2總是能獲取到最新的值。
事實(shí)上,多個(gè)線程都能寫(xiě)入共享的volatile變量,主存中也能存儲(chǔ)正確的變量值,然而這有一個(gè)前提,變量新值的寫(xiě)入不能依賴(lài)于變量的舊值。換句話說(shuō),就是一個(gè)線程寫(xiě)入一個(gè)共享volatile變量值時(shí),不需要先讀取變量值,然后以此來(lái)計(jì)算出新的值。
如果線程需要先讀取一個(gè)volatile變量的值,以此來(lái)計(jì)算出一個(gè)新的值,那么volatile變量就不足夠保證正確的可見(jiàn)性。(線程間)讀寫(xiě)volatile變量的時(shí)間間隔很短,這將導(dǎo)致一個(gè)競(jìng)態(tài)條件,多個(gè)線程同時(shí)讀取了volatile變量相同的值,然后以此計(jì)算出了新的值,這時(shí)各個(gè)線程往主存中寫(xiě)回值,則會(huì)互相覆蓋。
多個(gè)線程對(duì)counter變量進(jìn)行自增操作就是這樣的情形,下面的章節(jié)會(huì)詳細(xì)說(shuō)明這種情形。
設(shè)想一下,如果線程1將共享變量counter的值0讀取到它的CPU緩存,然后自增為1,而還沒(méi)有將新值寫(xiě)回到主存。線程2這時(shí)從主存中讀取的counter值依然是0,依然放到它自身的CPU緩存中,然后同樣將counter值自增為1,同樣也還沒(méi)有將新值寫(xiě)回到主存。如下圖所示:
從實(shí)際的情況來(lái)看,線程1和線程2現(xiàn)在就是不同步的。共享變量counter正確的值應(yīng)該是2,但各個(gè)線程中CPU緩存的值都是1,而主存中的值依然是0。這是很混亂的。即使線程最終將共享變量counter的值寫(xiě)回到主存,那值也明顯是錯(cuò)的。
何時(shí)使用volatile
正如我前面所說(shuō),如果兩個(gè)線程同時(shí)讀寫(xiě)一個(gè)共享變量,僅僅使用volatile關(guān)鍵字是不夠的。你應(yīng)該使用 synchronized 來(lái)保證讀寫(xiě)變量是原子的。(一個(gè)線程)讀寫(xiě)volatile變量時(shí),不會(huì)阻塞(其他)線程進(jìn)行讀寫(xiě)。你必須在關(guān)鍵的地方使用synchronized關(guān)鍵字來(lái)解決這個(gè)問(wèn)題。
除了synchronized方法,你還可以使用java.util.concurrent包提供的許多原子數(shù)據(jù)類(lèi)型來(lái)解決這個(gè)問(wèn)題。比如,AtomicLong或AtomicReference,或是其他的類(lèi)。
如果只有一個(gè)線程對(duì)volatile進(jìn)行讀寫(xiě),而其他線程只是讀取變量,這時(shí),對(duì)于只是讀取變量的線程來(lái)說(shuō),volatile就已經(jīng)可以保證讀取到的是變量的最新值。如果沒(méi)有把變量聲明為volatile,這就無(wú)法保證。
volatile關(guān)鍵字對(duì)32位和64位的變量都有效。
volatile的性能考量
讀寫(xiě)volatile變量會(huì)導(dǎo)致變量從主存讀寫(xiě)。從主存讀寫(xiě)比從CPU緩存讀寫(xiě)更加“昂貴”。訪問(wèn)一個(gè)volatile變量同樣會(huì)禁止指令重排,而指令重排是一種提升性能的技術(shù)。因此,你應(yīng)當(dāng)只在需要保證變量可見(jiàn)性的情況下,才使用volatile變量。
- - -END?- - -喜歡本文的朋友,歡迎關(guān)注公眾號(hào)??并發(fā)編程網(wǎng),收看更多精彩內(nèi)容
總結(jié)
以上是生活随笔為你收集整理的关键字_Java Volatile关键字的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 怎么打开北信源加密u盘_全国首个!北信源
- 下一篇: java美元兑换,(Java实现) 美元