volatile的實(shí)現(xiàn)原理與應(yīng)用

Java代碼在編譯后會變成Java字節(jié)碼,字節(jié)碼被類加載器加載到JVM里拱礁,JVM執(zhí)行字節(jié)碼然爆,最終需要轉(zhuǎn)化為匯編指令在CPU上執(zhí)行,Java中所使用的并發(fā)機(jī)制依賴于JVM的實(shí)現(xiàn)和CPU的指令搔涝。

volatile的應(yīng)用

在多線程并發(fā)編程中synchronized和volatile都扮演著重要的角色厨喂,volatile是輕量級的synchronized,它在多處理器開發(fā)中保證了共享變量的“可見性”庄呈⊥苫停可見性的意思是當(dāng)一個(gè)線程修改一個(gè)共享變量時(shí),另外一個(gè)線程能讀到這個(gè)修改的值诬留。如果volatile變量修飾符使用恰當(dāng)?shù)脑捫奔停萻ynchronized的使用和執(zhí)行成本更低,因?yàn)樗粫鹁€程上下文的切換和調(diào)度文兑。

1.volatile的定義與實(shí)現(xiàn)原理

Java語言規(guī)范第3版中對volatile的定義如下:Java編程語言允許線程訪問共享變量盒刚,為了確保共享變量能被準(zhǔn)確和一致地更新,線程應(yīng)該確保通過排他鎖單獨(dú)獲得這個(gè)變量绿贞。Java語言提供了volatile因块,在某些情況下比鎖要更加方便。如果一個(gè)字段被聲明成volatile樟蠕,Java線程內(nèi)存模型確保所有線程看到這個(gè)變量的值是一致的贮聂。

在了解volatile實(shí)現(xiàn)原理之前,我們先來看下與其實(shí)現(xiàn)原理相關(guān)的CPU術(shù)語與說明寨辩。

術(shù)語 英文單詞 術(shù)語描述
內(nèi)存屏障 memory barriers 是一組處理器指令吓懈,用于實(shí)現(xiàn)對內(nèi)存操作的順序限制
緩沖行 cache line 緩存中可以分配的最小存儲單位。處理器填寫緩存線時(shí)會加載整個(gè)緩存線靡狞,需要使用多個(gè)主內(nèi)存讀周期
原子操作 atomic operations 不可中斷的一個(gè)或一系列操作
緩沖行填充 cache line fill 當(dāng)處理器識別到從內(nèi)存中讀取操作數(shù)是可緩存的耻警,處理器讀取整個(gè)緩存行到適當(dāng)?shù)木彺妫↙1,L2甸怕,L3的或所有)
緩存命中 cache hit 如果進(jìn)行告訴緩存行填充操作的內(nèi)存位置仍然是下次處理器訪問的地址時(shí)甘穿,處理器從緩存中讀取操作數(shù),而不是從內(nèi)存讀取
寫命中 write hit 當(dāng)處理器將操作數(shù)寫回到一個(gè)內(nèi)存緩存的區(qū)域時(shí)梢杭,它首先會檢查這個(gè)緩存的內(nèi)存地址是否在緩存行中温兼,如果存在一個(gè)有效的緩存行,則處理器將這個(gè)操作數(shù)寫回到緩存武契,而不是寫回到內(nèi)存募判,這個(gè)操作被稱為寫命中
寫缺失 write misses the cache 一個(gè)有效的緩存行被寫入到不存在的內(nèi)存區(qū)域

volatile是如何來保證可見性的呢荡含?讓我們在X86處理器下通過工具獲取JIT編譯器生成的匯編指令來查看對volatile進(jìn)行寫操作時(shí),CPU會做什么事情届垫。

Java代碼如下:

instance = new Singleton(); //instance是volatile變量

轉(zhuǎn)變成匯編代碼释液,如下:

0x01a3de1d: movb $0x0,0x1104800(%esi);

oxo1a3de24: lock add1 $0x0,(%esp);

有volatile變量修飾的共享變量進(jìn)行寫操作的時(shí)候會多出第二行匯編代碼,通過查IA-32架構(gòu)軟件開發(fā)者手冊可知装处,Lock前綴的指令在多核處理器下會引發(fā)了兩件事情误债。

  1. 將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存。

  2. 這個(gè)寫回內(nèi)存的操作會使在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效妄迁。

為了提高處理速度寝蹈,處理器不直接和內(nèi)存進(jìn)行通信,而是先將系統(tǒng)內(nèi)存的數(shù)據(jù)讀到內(nèi)存緩存(L1判族,L2或其他)后再進(jìn)行操作躺盛,但操作完不知道何時(shí)會寫到內(nèi)存。如果對聲明了volatile的變量進(jìn)行寫操作形帮,JVM就會向處理器發(fā)送一條Lock前綴的指令槽惫,將這個(gè)變量所在緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存。但是就算寫回到內(nèi)存辩撑,如果其他處理器緩存的值還是舊的界斜,再執(zhí)行計(jì)算操作就會有問題。所以合冀,在多處理器下各薇,為了保證各個(gè)處理器的緩存是一致的,就會實(shí)現(xiàn)緩存一致性協(xié)議君躺,每個(gè)處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己緩存的值是不是過期了峭判,當(dāng)處理器發(fā)現(xiàn)自己緩存行對應(yīng)的內(nèi)存地址被修改,就會將當(dāng)前處理器的緩存行設(shè)置成無效狀態(tài)棕叫,當(dāng)處理器對這個(gè)數(shù)據(jù)進(jìn)行修改操作的時(shí)候林螃,會重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里。

下面來具體講解volatile的兩條實(shí)現(xiàn)原則俺泣。

  1. Lock前綴指令會引起處理器緩存回寫到內(nèi)存疗认。Lock前綴指令導(dǎo)致在執(zhí)行指令期間,聲名處理器的LOCK#信號伏钠。在多處理器環(huán)境中横漏,LOCK#信號確保在聲名該信號期間,處理器可以獨(dú)占任何共享內(nèi)存熟掂。但是缎浇,在最近的處理器里,LOCK#信號一般不鎖總線赴肚,而是鎖緩存华畏,畢竟鎖總線開銷的比較大鹏秋。對于Intel486和Pentium處理器尊蚁,在鎖操作時(shí)亡笑,總是在總線上聲明LOCK#信號。但在P6和目前的處理器中横朋,如果訪問的內(nèi)存區(qū)域已經(jīng)緩存在處理器內(nèi)部仑乌,則不會聲明LOCK#信號。相反琴锭,它會鎖定這塊內(nèi)存區(qū)域的緩存并會寫到內(nèi)存晰甚,并使用緩存一致性機(jī)制來確保修改的原子性,此操作被稱為“緩存鎖定”决帖,緩存一致性機(jī)制會阻止同時(shí)修改由兩個(gè)以上處理器緩存的內(nèi)存區(qū)域數(shù)據(jù)厕九。

  2. 一個(gè)處理器的緩存回寫到內(nèi)存會導(dǎo)致其他處理器的緩存無效。IA-32處理器和inter 64處理器使用MESI(修改地回、獨(dú)占扁远、共享、無效)控制協(xié)議去維護(hù)內(nèi)部緩存和其他處理器緩存的一致性刻像。在多核處理器系統(tǒng)中進(jìn)行操作的時(shí)候畅买,IA-32和inter 64處理器能嗅探其他處理器訪問系統(tǒng)內(nèi)存和它們的內(nèi)部緩存。處理器使用嗅探技術(shù)保證它的內(nèi)部緩存细睡、系統(tǒng)內(nèi)存和其他處理器的緩存的數(shù)據(jù)在總線上保持一致谷羞。例如,在Pentium和P6 family處理器中溜徙,如果通過嗅探一個(gè)處理器來檢測其他處理器打算寫內(nèi)存地址湃缎,而這個(gè)地址當(dāng)前處于共享狀態(tài),那么正在嗅探的處理器將使它的緩存行無效蠢壹,在下次訪問相同內(nèi)存地址時(shí)嗓违,強(qiáng)制執(zhí)行緩存行填充。

2.volatile的使用優(yōu)化

著名的Java并發(fā)編程大師Dourglea在JDK7的并發(fā)包里新增一個(gè)隊(duì)列集合類LinkedTransferQueue知残,它在使用volatile變量時(shí)靠瞎,用一種追加字節(jié)的方式來優(yōu)化隊(duì)列出隊(duì)和入隊(duì)的性能。LinkedTransferQueue的代碼如下

//隊(duì)列中的頭部節(jié)點(diǎn)
private transient final PaddedAtomicReference<QNode> head;
//隊(duì)列中的尾部節(jié)點(diǎn)
private transient final PaddedAtomicReferfence<QNode> tail;
static final class PaddedAtomicReference<T> extends AtomicReference {
    //使用很多4個(gè)字節(jié)的引用追加64個(gè)字節(jié)
    Object p0,p1,p2,p3,p4,p5,p6,p7,p8,p9,pa,pb,pc,pd,pe;
    PaddedAtomicReference(T r){
        super(r);
    }
}
public class AtomicReference <V> implements java.io.Serializable{
    private volatile V value;
    //省略其他代碼
}

追加字節(jié)能優(yōu)化性能求妹?這種方式看起來很神奇乏盐,但如果深入理解處理器架構(gòu)就能理解其中的奧秘。讓我們先來看看LinkedTransferQueue這個(gè)類制恍,它使用一個(gè)內(nèi)部類類型來定義隊(duì)列的頭節(jié)點(diǎn)(head)和尾節(jié)點(diǎn)(tail)父能,而這個(gè)內(nèi)部類PaddedAtomicReference相對于父類AtomicReference只做了一件事情,就是將共享變量追加到64字節(jié)净神。我們可以來計(jì)算下何吝,一個(gè)對象的引用占4個(gè)字節(jié)溉委,它追加了15個(gè)變量(共占60個(gè)字節(jié)),再加上父類的value變量爱榕,一個(gè)64個(gè)字節(jié)瓣喊。

為什么追加64字節(jié)能夠提高并發(fā)編程的效率呢?因?yàn)閷τ谟⑻貭柨犷7黔酥、酷睿藻三、Atom和NetBurst,以及Core Solo和Pentium M處理器的L1跪者、L2或L3緩存的高速緩存行是64個(gè)字節(jié)寬棵帽,不支持部分填充緩存行,這意味著渣玲,如果隊(duì)列的頭節(jié)點(diǎn)和尾節(jié)點(diǎn)都不足64字節(jié)的話逗概,處理器會將它們都讀到同一個(gè)高速緩存行中,在多處理器下每個(gè)處理器都會緩存同樣的頭忘衍、尾節(jié)點(diǎn)逾苫,當(dāng)一個(gè)處理器試圖修改頭節(jié)點(diǎn)時(shí),會將整個(gè)緩存行鎖定淑履,那么在緩存一致性機(jī)制的作用下隶垮,會導(dǎo)致其他處理器不能訪問自己高速緩存中的尾節(jié)點(diǎn),而隊(duì)列的入隊(duì)和出隊(duì)操作則需要不停修改頭節(jié)點(diǎn)和尾節(jié)點(diǎn)秘噪,所以在多處理器的情況下將會嚴(yán)重影響到隊(duì)列的入隊(duì)和出隊(duì)效率狸吞。Douglea使用追加到64字節(jié)的方式來填滿高速緩沖區(qū)的緩存行,避免頭節(jié)點(diǎn)和尾節(jié)點(diǎn)加載到同一個(gè)緩存行指煎,使頭蹋偏、尾節(jié)點(diǎn)在修改時(shí)不會互相鎖定。

那么是不是在使用volatile變量時(shí)都應(yīng)該追加到64字節(jié)呢至壤?不是的威始。在兩種場景下不應(yīng)該使用這種方式:

  1. 緩存行非64字節(jié)寬的處理器。如P6系列和奔騰處理器像街,它們的L1和L2告訴緩存行是32個(gè)字節(jié)寬黎棠。
  2. 共享變量不會被頻繁地寫。因?yàn)槭褂米芳幼止?jié)地方式需要處理器讀取更多的字節(jié)到高速緩沖區(qū)镰绎,這本身就會帶來一定的性能消耗脓斩,如果共享變量不被頻繁寫的話,鎖的幾率也非常小畴栖,就沒必要通過追加字節(jié)的方式來避免相互鎖定随静。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子燎猛,更是在濱河造成了極大的恐慌恋捆,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件重绷,死亡現(xiàn)場離奇詭異沸停,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)论寨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進(jìn)店門星立,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人葬凳,你說我怎么就攤上這事∈易啵” “怎么了火焰?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長胧沫。 經(jīng)常有香客問我昌简,道長,這世上最難降的妖魔是什么绒怨? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任纯赎,我火速辦了婚禮,結(jié)果婚禮上南蹂,老公的妹妹穿的比我還像新娘犬金。我一直安慰自己,他們只是感情好六剥,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布晚顷。 她就那樣靜靜地躺著,像睡著了一般疗疟。 火紅的嫁衣襯著肌膚如雪该默。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天策彤,我揣著相機(jī)與錄音栓袖,去河邊找鬼。 笑死店诗,一個(gè)胖子當(dāng)著我的面吹牛裹刮,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播必搞,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼必指,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了恕洲?” 一聲冷哼從身側(cè)響起塔橡,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤梅割,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后葛家,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體户辞,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年癞谒,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了底燎。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,424評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡弹砚,死狀恐怖双仍,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情桌吃,我是刑警寧澤朱沃,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站茅诱,受9級特大地震影響逗物,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜瑟俭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一翎卓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧摆寄,春花似錦失暴、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至畜号,卻和暖如春缴阎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背简软。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工蛮拔, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人痹升。 一個(gè)月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓建炫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親疼蛾。 傳聞我的和親對象是個(gè)殘疾皇子肛跌,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評論 2 359

推薦閱讀更多精彩內(nèi)容