Java Volatile Keyword
在這篇文章中,我們將關(guān)注Java語言中的基本但經(jīng)常被誤解的概念 - volatile
關(guān)鍵字。
1.概述
在Java中悉尾,每個(gè)線程都有一個(gè)獨(dú)立的內(nèi)存空間贺嫂,稱為工作內(nèi)存; 這保存了用于執(zhí)行操作的不同變量的值栏豺。在執(zhí)行操作之后,線程將變量的更新值復(fù)制到主存儲(chǔ)器,并且從那里其他線程可以讀取最新值。
Java volatile
關(guān)鍵字作用將Java變量標(biāo)記為“存儲(chǔ)在主存儲(chǔ)器中”絮缅。更確切地說鲁沥,這意味著,每次讀取一個(gè)volatile變量都將從計(jì)算機(jī)的主內(nèi)存中讀取耕魄,而不是從CPU緩存中讀取画恰,并且每次寫入volatile
變量都將寫入主內(nèi)存,而不僅僅是CPU緩存吸奴。
簡(jiǎn)單地說允扇,volatile關(guān)鍵字標(biāo)記一個(gè)變量,在多個(gè)線程訪問它的情況下则奥,總是轉(zhuǎn)到主內(nèi)存考润,讀取和寫入。
實(shí)際上读处,自Java 5以來额划,volatile
關(guān)鍵字保證的不僅僅是易失性變量被寫入主內(nèi)存并從主內(nèi)存中讀取。我將在以下部分解釋档泽。
可變可見性問題
Java volatile
關(guān)鍵字保證可以跨線程查看變量的變化。這可能聽起來有點(diǎn)抽象揖赴,所以讓我詳細(xì)說明馆匿。
在線程操作非易失性變量的多線程應(yīng)用程序中,出于性能原因燥滑,每個(gè)線程可以在處理它們時(shí)將變量從主內(nèi)存復(fù)制到CPU緩存中渐北。如果您的計(jì)算機(jī)包含多個(gè)CPU,則每個(gè)線程可以在不同的CPU上運(yùn)行铭拧。這意味著赃蛛,每個(gè)線程可以將變量復(fù)制到不同CPU的CPU緩存中。這在這里說明:
對(duì)于non-volatile變量呕臂,無法保證Java虛擬機(jī)(JVM)何時(shí)將數(shù)據(jù)從主內(nèi)存讀入CPU緩存,或?qū)?shù)據(jù)從CPU緩存寫入主內(nèi)存肪跋。這可能會(huì)導(dǎo)致幾個(gè)問題歧蒋,我將在以下部分中解釋。
想象一下兩個(gè)或多個(gè)線程可以訪問共享對(duì)象的情況州既,該共享對(duì)象包含一個(gè)聲明如下的計(jì)數(shù)器變量:
public class SharedObject {
public int counter = 0;
}
想象一下谜洽,只有線程1遞增counter
變量,但線程1和線程2都可能counter
不時(shí)讀取變量吴叶。
如果counter
變量未聲明為volatile
阐虚,則無法保證何時(shí)將counter
變量的值從CPU緩存寫回主內(nèi)存。這意味著counter
變量在CPU緩存中的變量值可能與主存儲(chǔ)器中的變量值不同蚌卤。這種情況如下所示:
這里的問題是奥秆,其他線程沒有看到counter
變量的最新值,原因是它還沒有被另一個(gè)線程寫回主內(nèi)存磕洪,稱為“可見性”問題吭练。其他線程看不到一個(gè)線程的更新。
2.易失性和線程同步
對(duì)于所有多線程應(yīng)用程序析显,我們需要確保一致的行為規(guī)則:
- 相互排斥 - 一次只有一個(gè)線程執(zhí)行一個(gè)關(guān)鍵部分
- 可見性 - 一個(gè)線程對(duì)共享數(shù)據(jù)所做的更改對(duì)其他線程可見鲫咽,以維護(hù)數(shù)據(jù)一致性
同步方法和塊提供上述兩種屬性,但代價(jià)是應(yīng)用程序的性能谷异。
Volatile是一個(gè)非常有用的原語分尸,因?yàn)樗梢詭椭?strong>確保數(shù)據(jù)變化的可見性方面,當(dāng)然歹嘹,不提供互斥箩绍。因此,它在我們可以使用多個(gè)線程并行執(zhí)行代碼塊但我們需要確背呱希可見性屬性的地方很有用材蛛。
Java易失性可見性保證
Java volatile關(guān)鍵字旨在解決可變可見性問題。通過聲明counter變量為volatile怎抛,對(duì)counter變量的所有寫操作都將立即寫回內(nèi)存卑吭。此外,counter變量的所有讀取都將直接從主存儲(chǔ)器中讀取马绝。
以下是 變量volatile聲明的counter樣子:
public class SharedObject {
public volatile int counter = 0;
}
聲明volatile變量可以保證對(duì)該變量的其他寫入線程的可見性豆赏。
在上面給出的場(chǎng)景中,一個(gè)線程(T1)修改計(jì)數(shù)器富稻,另一個(gè)線程(T2)讀取計(jì)數(shù)器(但從不修改它)掷邦,聲明該counter變量volatile后足以保證counter變量的寫入對(duì)T2是可見的。
但是椭赋,如果T1和T2都在增加counter變量抚岗,那么聲明 counter變量volatile就不夠了。稍后會(huì)詳細(xì)介紹哪怔。
完全不穩(wěn)定的可見性保證
實(shí)際上苟跪,Java volatile的可見性保證超出了volatile 變量本身。能見度保證如下情形:
如果線程A寫入volatile變量并且線程B隨后讀取相同的volatile變量蔓涧,線程A在寫入之前的所有volatile變量件已,當(dāng)線程B讀取volatile變量后也將對(duì)線程B可見。
如果線程A讀取volatile變量元暴,則讀取變量時(shí)線程A可見的所有變量volatile也將從主存儲(chǔ)器重新讀取篷扩。
讓我用代碼示例說明:
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;
}
}
該udpate()方法寫入三個(gè)變量,其中只有days 是volatile茉盏。
完全volatile可見性保證意味著鉴未,當(dāng)寫入days值時(shí)枢冤,線程可見的所有變量也會(huì)寫入主存儲(chǔ)器。這意味著铜秆,當(dāng)days的值被寫入主內(nèi)存淹真,years和months的值也被寫入主存儲(chǔ)器。
當(dāng)讀取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;
}
}
注意totalDays()通過讀取的值的方法開始days到 total變量核蘸。當(dāng)讀取的值days,值months 和years也被讀入到主存儲(chǔ)器中啸驯。因此可以保證看到的最新值days客扎,months并years與上述讀取序列。
指令重新排序挑戰(zhàn)
只要指令的語義含義保持不變罚斗,Java 虛擬機(jī)和CPU就可以出于性能原因重新排序程序中的指令徙鱼。例如,請(qǐng)查看以下說明:
int a = 1;
int b = 2;
a++;
b++;
這些指令可以按以下順序重新排序针姿,而不會(huì)丟失程序的語義含義:`
int a = 1;
a++;
int b = 2;
b++;
然而袱吆,當(dāng)其中一個(gè)變量是volatile變量時(shí),指令重新排序時(shí)將面臨挑戰(zhàn)距淫。讓我們看看MyClass這個(gè)Java volatile教程前面的例子中的類:
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;
}
}
一旦調(diào)用update()方法寫入一個(gè)值到days, 新寫入到y(tǒng)ears 和months的值同樣被寫入到主內(nèi)存杆故。
但是,如果Java VM重新排序指令溉愁,如下所示:
public void update(int years, int months, int days){
this.days = days;
this.months = months;
this.years = years;
}
當(dāng)我們?nèi)バ薷膁ays變量時(shí)months的值和years值仍寫入到主內(nèi)存,但這次發(fā)生的是days修改是發(fā)生在寫months years之前饲趋。新的值(months,years)不能正確被其他線程可見.重新排序的指令的語義含義已經(jīng)改變拐揭。
Java有一個(gè)解決這個(gè)問題的方法,我們將在下一節(jié)中看到奕塑。
3.發(fā)生在保證之前
從Java 5開始堂污,volatile關(guān)鍵字還提供了額外的功能,可確保包括非易失性變量在內(nèi)的所有變量的值與Volatile寫操作一起寫入主存儲(chǔ)器龄砰。
這稱為Happens-Before盟猖,因?yàn)樗鼮樗凶兞刻峁┝藢?duì)另一個(gè)讀取線程的可見性。此外换棚,JVM不會(huì)重新排序volatile變量的讀寫指令式镐。
Java volatile Happens-Before Guarantee
為了解決指令重新排序挑戰(zhàn),volatile除了可見性保證之外固蚤,Java 關(guān)鍵字還提供“先發(fā)生”保證娘汞。事先發(fā)生的保證保證:
1.如果讀取/寫入最初發(fā)生在對(duì)volatile變量寫入之前,則無法重新排序?qū)ζ渌兞康淖x和寫操作夕玩。
對(duì)volatile變量的寫入之前的讀/寫將會(huì)保證在寫入”volatile"之前“先發(fā)生”你弦。
請(qǐng)注意惊豺,在寫入“volatile"變量之前,可能會(huì)對(duì)其他變量的讀/寫進(jìn)行重新排序,以使其在寫入“volatile"后發(fā)生禽作。只是不是另一種方式尸昧。從以后到以前是允許的, 但從以前到以后是不允許的。
2.如果讀取/寫入最初發(fā)生在對(duì)volatile變量讀取之后旷偿,則無法重新排序?qū)ζ渌兞康淖x和寫操作烹俗。
請(qǐng)注意, 在讀取“volatile"變量之前, 可能會(huì)對(duì)其他變量的讀取進(jìn)行重新排序, 以使其在讀取“volatile"后發(fā)生。只是不是另一種方式狸捅。從之前到以后是允許的, 但從以后到以前是不允許的衷蜓。
上述情況-在保證確保volatile關(guān)鍵字的可見性保證被強(qiáng)制執(zhí)行之前。
volatile is Not Always Enough
即使volatile
關(guān)鍵字保證volatile
變量直接從主存儲(chǔ)器讀取變量的所有讀取尘喝,并且所有對(duì)volatile
變量的寫入都直接寫入主存儲(chǔ)器磁浇,仍然存在聲明變量不足的情況volatile
。
在前面解釋的情況中朽褪,只有線程1寫入共享counter
變量置吓,聲明該counter
變量volatile
足以確保線程2始終看到最新的寫入值。
實(shí)際上缔赠,volatile
如果寫入變量的新值不依賴于其先前的值衍锚,則多個(gè)線程甚至可以寫入共享變量,并且仍然具有存儲(chǔ)在主存儲(chǔ)器中的正確值嗤堰。換句話說戴质,如果將值寫入共享volatile
變量的線程首先不需要讀取其值來計(jì)算其下一個(gè)值。
一旦線程需要首先讀取volatile
變量的值踢匣,并且基于該值為共享volatile
變量生成新值告匠,volatile
變量就不再足以保證正確的可見性。讀取volatile
變量和寫入新值之間的短時(shí)間間隔會(huì)產(chǎn)生競(jìng)爭(zhēng)條件 离唬,其中多個(gè)線程可能讀取volatile
變量的相同值后专,為變量生成新值,并在將值寫回時(shí)主存 - 覆蓋彼此的值输莺。
多個(gè)線程遞增相同計(jì)數(shù)器的情況恰好是 volatile
變量不夠的情況戚哎。以下部分更詳細(xì)地解釋了這種情況。
想象一下嫂用,如果線程1將counter
值為0 的共享變量讀入其CPU高速緩存型凳,則將其增加到1并且不將更改的值寫回主存儲(chǔ)器。然后嘱函,線程2可以counter
從主存儲(chǔ)器讀取相同的變量啰脚,其中變量的值仍為0,進(jìn)入其自己的CPU高速緩存。然后橄浓,線程2也可以將計(jì)數(shù)器遞增到1粒梦,也不將其寫回主存儲(chǔ)器。這種情況如下圖所示:
線程1和線程2現(xiàn)在幾乎不同步匀们。共享counter
變量的實(shí)際值應(yīng)為2,但每個(gè)線程的CPU緩存中的變量值為1准给,而主存中的值仍為0.這是一個(gè)混亂泄朴!即使線程最終將共享counter
變量的值寫回主存儲(chǔ)器,該值也將是錯(cuò)誤的露氮。
什么時(shí)候揮發(fā)夠了祖灰?
正如我前面提到的,如果兩個(gè)線程都在讀取和寫入共享變量畔规,那么使用 volatile
關(guān)鍵字是不夠的局扶。 在這種情況下,您需要使用synchronized來保證變量的讀取和寫入是原子的叁扫。讀取或?qū)懭雟olatile變量不會(huì)阻止線程讀取或?qū)懭肴琛榇耍仨?code>synchronized 在關(guān)鍵部分代碼
作為synchronized
塊的替代方法莫绣,您還可以使用java.util.concurrent
包中提供的原子數(shù)據(jù)類型畴蒲。例如,AtomicLong
或者 AtomicReference
來避免競(jìng)爭(zhēng)條件对室。
- 標(biāo)記為synchronized的邏輯變?yōu)橥綁K模燥,在任何給定時(shí)間只允許一個(gè)線程執(zhí)行。
如果只有一個(gè)線程讀取和寫入volatile變量的值掩宜,而其他線程只讀取變量蔫骂,那么讀取線程將保證看到寫入volatile變量的最新值。如果不使變量變?yōu)関olatile锭亏,則無法保證。
該volatile
關(guān)鍵字保證適用于32位和64位變量硬鞍。
揮發(fā)性的性能考慮因素
讀取和寫入volatile變量會(huì)導(dǎo)致變量被讀取或?qū)懭胫鞔鎯?chǔ)器慧瘤。讀取和寫入主內(nèi)存比訪問CPU緩存更昂貴。訪問volatile變量也會(huì)阻止指令重新排序固该,這是一種正常的性能增強(qiáng)技術(shù)锅减。因此,在真正需要強(qiáng)制實(shí)施變量可見性時(shí)伐坏,應(yīng)該只使用volatile變量怔匣。