Java語(yǔ)言為了解決并發(fā)編程中存在的原子性矿筝、可見性和有序性問題,提供了一系列和并發(fā)處理相關(guān)的關(guān)鍵字棚贾,比如synchronized窖维、volatile、final鸟悴、concurren包等陈辱。在前一篇文章中,我們也介紹了synchronized的用法及原理细诸。本文,來(lái)分析一下另外一個(gè)關(guān)鍵字——volatile陋守。
本文就圍繞volatile展開震贵,主要介紹volatile的用法、volatile的原理水评,以及volatile是如何提供可見性和有序性保障的等猩系。
volatile這個(gè)關(guān)鍵字,不僅僅在Java語(yǔ)言中有中燥,在很多語(yǔ)言中都有的寇甸,而且其用法和語(yǔ)義也都是不盡相同的。尤其在C語(yǔ)言疗涉、C++以及Java中拿霉,都有volatile關(guān)鍵字。都可以用來(lái)聲明變量或者對(duì)象咱扣。下面簡(jiǎn)單來(lái)介紹一下Java語(yǔ)言中的volatile關(guān)鍵字绽淘。
volatile的用法
volatile通常被比喻成"輕量級(jí)的synchronized",也是Java并發(fā)編程中比較重要的一個(gè)關(guān)鍵字闹伪。和synchronized不同沪铭,volatile是一個(gè)變量修飾符,只能用來(lái)修飾變量偏瓤。無(wú)法修飾方法及代碼塊等杀怠。
volatile的用法比較簡(jiǎn)單,只需要在聲明一個(gè)可能被多線程同時(shí)訪問的變量時(shí)厅克,使用volatile修飾就可以了赔退。
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
如以上代碼,是一個(gè)比較典型的使用雙重鎖校驗(yàn)的形式實(shí)現(xiàn)單例的已骇,其中使用volatile關(guān)鍵字修飾可能被多個(gè)線程同時(shí)訪問到的singleton离钝。
volatile的原理
在再有人問你Java內(nèi)存模型是什么票编,就把這篇文章發(fā)給他中我們?cè)?jīng)介紹過(guò),為了提高處理器的執(zhí)行速度卵渴,在處理器和內(nèi)存之間增加了多級(jí)緩存來(lái)提升慧域。但是由于引入了多級(jí)緩存,就存在緩存數(shù)據(jù)不一致問題浪读。
但是昔榴,對(duì)于volatile變量,當(dāng)對(duì)volatile變量進(jìn)行寫操作的時(shí)候碘橘,JVM會(huì)向處理器發(fā)送一條lock前綴的指令互订,將這個(gè)緩存中的變量回寫到系統(tǒng)主存中。
但是就算寫回到內(nèi)存痘拆,如果其他處理器緩存的值還是舊的仰禽,再執(zhí)行計(jì)算操作就會(huì)有問題,所以在多處理器下纺蛆,為了保證各個(gè)處理器的緩存是一致的吐葵,就會(huì)實(shí)現(xiàn)緩存一致性協(xié)議
緩存一致性協(xié)議:每個(gè)處理器通過(guò)嗅探在總線上傳播的數(shù)據(jù)來(lái)檢查自己緩存的值是不是過(guò)期了,當(dāng)處理器發(fā)現(xiàn)自己緩存行對(duì)應(yīng)的內(nèi)存地址被修改桥氏,就會(huì)將當(dāng)前處理器的緩存行設(shè)置成無(wú)效狀態(tài)温峭,當(dāng)處理器要對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作的時(shí)候,會(huì)強(qiáng)制重新從系統(tǒng)內(nèi)存里把數(shù)據(jù)讀到處理器緩存里字支。
所以凤藏,如果一個(gè)變量被volatile所修飾的話,在每次數(shù)據(jù)變化之后堕伪,其值都會(huì)被強(qiáng)制刷入主存揖庄。而其他處理器的緩存由于遵守了緩存一致性協(xié)議,也會(huì)把這個(gè)變量的值從主存加載到自己的緩存中刃跛。這就保證了一個(gè)volatile在并發(fā)編程中抠艾,其值在多個(gè)緩存中是可見的。
volatile與可見性
可見性是指當(dāng)多個(gè)線程訪問同一個(gè)變量時(shí)桨昙,一個(gè)線程修改了這個(gè)變量的值检号,其他線程能夠立即看得到修改的值。
我們?cè)谠儆腥藛柲鉐ava內(nèi)存模型是什么蛙酪,就把這篇文章發(fā)給他中分析過(guò):Java內(nèi)存模型規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存中齐苛,每條線程還有自己的工作內(nèi)存,線程的工作內(nèi)存中保存了該線程中是用到的變量的主內(nèi)存副本拷貝桂塞,線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行凹蜂,而不能直接讀寫主內(nèi)存。不同的線程之間也無(wú)法直接訪問對(duì)方工作內(nèi)存中的變量,線程間變量的傳遞均需要自己的工作內(nèi)存和主存之間進(jìn)行數(shù)據(jù)同步進(jìn)行玛痊。所以汰瘫,就可能出現(xiàn)線程1改了某個(gè)變量的值,但是線程2不可見的情況擂煞。
前面的關(guān)于volatile的原理中介紹過(guò)了混弥,Java中的volatile關(guān)鍵字提供了一個(gè)功能,那就是被其修飾的變量在被修改后可以立即同步到主內(nèi)存对省,被其修飾的變量在每次是用之前都從主內(nèi)存刷新蝗拿。因此,可以使用volatile來(lái)保證多線程操作時(shí)變量的可見性蒿涎。
volatile與有序性
有序性即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行哀托。
我們?cè)谠儆腥藛柲鉐ava內(nèi)存模型是什么,就把這篇文章發(fā)給他中分析過(guò):除了引入了時(shí)間片以外劳秋,由于處理器優(yōu)化和指令重排等仓手,CPU還可能對(duì)輸入代碼進(jìn)行亂序執(zhí)行,比如load->add->save 有可能被優(yōu)化成load->save->add 玻淑。這就是可能存在有序性問題俗或。
而volatile除了可以保證數(shù)據(jù)的可見性之外,還有一個(gè)強(qiáng)大的功能岁忘,那就是他可以禁止指令重排優(yōu)化等。
普通的變量?jī)H僅會(huì)保證在該方法的執(zhí)行過(guò)程中所依賴的賦值結(jié)果的地方都能獲得正確的結(jié)果区匠,而不能保證變量的賦值操作的順序與程序代碼中的執(zhí)行順序一致干像。
volatile可以禁止指令重排,這就保證了代碼的程序會(huì)嚴(yán)格按照代碼的先后順序執(zhí)行驰弄。這就保證了有序性麻汰。被volatile修飾的變量的操作,會(huì)嚴(yán)格按照代碼順序執(zhí)行戚篙,load->add->save 的執(zhí)行順序就是:load五鲫、add、save岔擂。
volatile與原子性
原子性是指一個(gè)操作是不可中斷的位喂,要全部執(zhí)行完成,要不就都不執(zhí)行乱灵。
我們?cè)贘ava的并發(fā)編程中的多線程問題到底是怎么回事兒中分析過(guò):線程是CPU調(diào)度的基本單位塑崖。CPU有時(shí)間片的概念,會(huì)根據(jù)不同的調(diào)度算法進(jìn)行線程調(diào)度痛倚。當(dāng)一個(gè)線程獲得時(shí)間片之后開始執(zhí)行规婆,在時(shí)間片耗盡之后,就會(huì)失去CPU使用權(quán)。所以在多線程場(chǎng)景下抒蚜,由于時(shí)間片在線程間輪換掘鄙,就會(huì)發(fā)生原子性問題。
在上一篇文章中嗡髓,我們介紹synchronized的時(shí)候操漠,提到過(guò),為了保證原子性器贩,需要通過(guò)字節(jié)碼指令monitorenter和monitorexit颅夺,但是volatile和這兩個(gè)指令之間是沒有任何關(guān)系的。
所以蛹稍,volatile是不能保證原子性的吧黄。
在以下兩個(gè)場(chǎng)景中可以使用volatile來(lái)代替synchronized:
1、運(yùn)算結(jié)果并不依賴變量的當(dāng)前值唆姐,或者能夠確保只有單一的線程會(huì)修改變量的值拗慨。
2、變量不需要與其他狀態(tài)變量共同參與不變約束奉芦。
除以上場(chǎng)景外赵抢,都需要使用其他方式來(lái)保證原子性,如synchronized或者concurrent包声功。
我們來(lái)看一下volatile和原子性的例子:
public class Test {
public volatile int i = 0;
public void increase() {
i++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保證前面的線程都執(zhí)行完
Thread.yield();
System.out.println(test.i);
}
}
以上代碼比較簡(jiǎn)單烦却,就是創(chuàng)建10個(gè)線程,然后分別執(zhí)行1000次i++操作先巴。正常情況下其爵,程序的輸出結(jié)果應(yīng)該是10000,但是伸蚯,多次執(zhí)行的結(jié)果都小于10000摩渺。這其實(shí)就是volatile無(wú)法滿足原子性的原因。
為什么會(huì)出現(xiàn)這種情況呢剂邮,那就是因?yàn)殡m然volatile可以保證i在多個(gè)線程之間的可見性摇幻。但是無(wú)法保證i++的原子性。
i++操作挥萌,一共有三個(gè)步驟:load i 绰姻,add i ,save i。在多線程場(chǎng)景中瑞眼,如果這三個(gè)步驟無(wú)法按照順序執(zhí)行的話龙宏,那么就會(huì)出現(xiàn)問題。
如上圖伤疙,兩個(gè)線程同時(shí)執(zhí)行i++操作银酗,如果允許指令重排辆影,我們期望的結(jié)果是3,但是實(shí)際執(zhí)行結(jié)果可能是2黍特,甚至可能是1蛙讥。
總結(jié)與思考
我們介紹過(guò)了volatile關(guān)鍵字和synchronized關(guān)鍵字。現(xiàn)在我們知道灭衷,synchronized可以保證原子性次慢、有序性和可見性。而volatile卻只能保證有序性和可見性翔曲。在這里順便給大家推薦一個(gè)架構(gòu)交流群:617434785迫像,里面會(huì)分享一些資深架構(gòu)師錄制的視頻錄像:有Spring,MyBatis瞳遍,Netty源碼分析闻妓,高并發(fā)、高性能掠械、分布式由缆、微服務(wù)架構(gòu)的原理,JVM性能優(yōu)化這些成為架構(gòu)師必備的知識(shí)體系猾蒂。還能領(lǐng)取免費(fèi)的學(xué)習(xí)資源均唉。相信對(duì)于已經(jīng)工作和遇到技術(shù)瓶頸的碼友,在這個(gè)群里會(huì)有你需要的內(nèi)容肚菠。
那么舔箭,我們?cè)賮?lái)看一下雙重校驗(yàn)鎖實(shí)現(xiàn)的單例,已經(jīng)使用了synchronized蚊逢,為什么還需要volatile限嫌?
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}