volatile的作用
1纲辽、保證變量可見性
說到volatile,就不得不提一個(gè)詞:“可見性”,可見性是指當(dāng)多個(gè)線程訪問同一個(gè)變量時(shí)轧邪,一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看得到修改的值羞海。
要具體理解這句話的含義忌愚,還需要看下Java的內(nèi)存模型:
由于內(nèi)存和cpu之間的計(jì)算速度差距過大,如果cpu直接從內(nèi)存中讀取數(shù)據(jù)十分影響性能却邓,所以在cpu和內(nèi)存之間加了一個(gè)溝通的橋梁——高速緩存硕糊。
Java內(nèi)存模型規(guī)定:
1.所有的變量都存儲(chǔ)在主內(nèi)存中;
2.線程擁有自己的工作內(nèi)存(即高速緩存)腊徙,線程的工作內(nèi)存中保存了該線程所使用到的變量(拷貝自主內(nèi)存)简十;
3.線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,不同線程之間也無法直接訪問對(duì)方工作內(nèi)存中的變量撬腾,線程間變量值的傳遞均需要通過主內(nèi)存來完成螟蝙。
單線程情況下,不會(huì)產(chǎn)生任何問題民傻,但當(dāng)多線程訪問同一變量時(shí)胰默,就有可能讀到臟數(shù)據(jù)。
假設(shè)有一個(gè)成員變量i饰潜,初值為0初坠,線程A和線程B同時(shí)修改一個(gè)變量i,首先彭雾,線程A碟刺、B將變量i從主存讀到工作內(nèi)存中,初值均為0薯酝,線程A將i+10半沽,線程B將i+1爽柒,此時(shí),由于線程A可能還沒有將工作內(nèi)存中的數(shù)據(jù)(i=10)刷新到主存者填,B沒有重新拷貝主存中的數(shù)據(jù)浩村,導(dǎo)致B線程看到的i仍然為0,也就是說占哟,A線程修改的變量對(duì)B線程“不可見”了心墅,即出現(xiàn)“臟讀”。
此時(shí)榨乎,如果對(duì)成員變量i加volatile關(guān)鍵字怎燥,同樣是上述場(chǎng)景,i在每次被線程訪問時(shí)蜜暑,都檢查變量的地址是否改變铐姚,如改變,就強(qiáng)迫從主內(nèi)存中重讀該成員變量的值肛捍,并且隐绵,當(dāng)成員變量發(fā)生變化時(shí),強(qiáng)迫線程將變化值回寫到主內(nèi)存中拙毫,這樣在任何時(shí)刻依许,AB線程總是看到某個(gè)成員變量的同一個(gè)值。
2恬偷、禁止指令重排序
1.當(dāng)程序執(zhí)行到volatile變量的讀操作或者寫操作時(shí)悍手,在其前面的操作的更改肯定全部已經(jīng)進(jìn)行,且結(jié)果已經(jīng)對(duì)后面的操作可見袍患;并且在其后面的操作肯定還沒有進(jìn)行坦康。
2.在進(jìn)行指令優(yōu)化時(shí),不能將在對(duì)volatile變量的讀操作或者寫操作的語句放在其后面執(zhí)行诡延,也不能把volatile變量后面的語句放到其前面執(zhí)行滞欠。
在Java內(nèi)存模型中,為了保證效率肆良,允許編譯器和處理器對(duì)指令進(jìn)行重排序筛璧,但是重排序過程不會(huì)影響到單線程程序的執(zhí)行,卻會(huì)影響到多線程并發(fā)執(zhí)行的正確性惹恃。
例如我們常見的雙重檢查單例:
為什么要給instance加上volatile關(guān)鍵字呢夭谤?這是由于instance=new Singleton();這句話不是原子操作,在JVM中巫糙,這句話被分為三個(gè)階段:
1.為instance分配內(nèi)存
2.初始化instance
3.將instance變量指向分配的內(nèi)存空間
由于JVM重排序的存在朗儒,上述23兩步操作是沒有依賴關(guān)系的,可能被重排序,也就是說醉锄,在instance還沒被初始化的時(shí)候乏悄,instance就已經(jīng)不為null了,這時(shí)恳不,另一個(gè)線程執(zhí)行到第一個(gè)if檩小,判斷不為null,直接return instance烟勋,導(dǎo)致異常规求。
volatile的實(shí)現(xiàn)原理
1、可見性
對(duì)聲明了volatile變量進(jìn)行寫操作時(shí)卵惦,JVM會(huì)向處理器發(fā)送一條Lock前綴的指令颓哮,將這個(gè)變量所在緩存行的數(shù)據(jù)回寫到系統(tǒng)內(nèi)存, 這一步確保了如果有其他線程對(duì)聲明了volatile變量進(jìn)行修改鸵荠,則立即更新主內(nèi)存中數(shù)據(jù)。
但這時(shí)候其他處理器的緩存還是舊的伤极,所以在多處理器環(huán)境下蛹找,為了保證各個(gè)處理器緩存一致,每個(gè)處理會(huì)通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己的緩存是否過期哨坪,當(dāng)處理器發(fā)現(xiàn)自己緩存行對(duì)應(yīng)的內(nèi)存地址被修改了庸疾,就會(huì)將當(dāng)前處理器的緩存行設(shè)置成無效狀態(tài),當(dāng)處理器要對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作時(shí)当编,會(huì)強(qiáng)制重新從系統(tǒng)內(nèi)存把數(shù)據(jù)讀到處理器緩存里届慈。 這一步確保了其他線程獲得的聲明了volatile變量都是從主內(nèi)存中獲取最新的。
2忿偷、有序性
Lock前綴指令實(shí)際上相當(dāng)于一個(gè)內(nèi)存屏障金顿,它確保指令重排序時(shí)不會(huì)把其后面的指令排到內(nèi)存屏障之前的位置,也不會(huì)把前面的指令排到內(nèi)存屏障的后面鲤桥;即在執(zhí)行到內(nèi)存屏障這句指令時(shí)揍拆,在它前面的操作已經(jīng)全部完成。
volatile的使用條件
1.對(duì)變量的寫操作不依賴于當(dāng)前值茶凳;
2.該變量沒有包含在具有其他變量的不變式中嫂拴。
volatile的注意點(diǎn)
上面一條講到了volatile關(guān)鍵字的使用條件,從注意點(diǎn)的角度來解釋一下贮喧。
首先筒狠,volatile雖然可以用在并發(fā)編程中,但是不能代替synchronized關(guān)鍵字箱沦,因?yàn)関olatile關(guān)鍵字只能保證并發(fā)編程中三大概念中的兩個(gè)辩恼,即可見性和有序性,但是它不能保證程序的原子性,舉例說明:
該例中运挫,inc的結(jié)果為10000嗎状共?不是,那問題出在哪里谁帕?
問題在于峡继,increase方法中,inc++這個(gè)操作不具備原子性匈挖,線程要先讀到位于主內(nèi)存中的inc的值碾牌,然后對(duì)其進(jìn)行+1,然后再將其放回主內(nèi)存中儡循,在這三步中舶吗,假設(shè)線程A讀取了inc,正在進(jìn)行+1操作择膝,此時(shí)誓琼,線程B讀取inc,由于線程A沒有操作完肴捉,所有并沒有將+1后的操作寫入主存腹侣,導(dǎo)致B讀取的仍為舊值,最終導(dǎo)致程序的結(jié)果小于預(yù)期結(jié)果齿穗,解決方法:可以將increase方法加上synchronized關(guān)鍵字傲隶,保證其原子性。
總結(jié)
本篇大致講解了volatile的用途以及實(shí)現(xiàn)原理窃页,上文也說到跺株,他不能代替synchronized關(guān)鍵字,在下篇文章中脖卖,就要講到Java中的各種鎖乒省,看下Java的鎖機(jī)制是如何保證效率的情況下,在多線程中安全運(yùn)行的畦木。