理解volatile關(guān)鍵字

volatile關(guān)鍵字

當(dāng)變量被某個(gè)線程A修改值之后,其它線程比如B若讀取此變量的話,立刻可以看到原來線程A修改后的值

注:普通變量與volatile變量的區(qū)別是volatile的特殊規(guī)則保證了新值能立即同步到主內(nèi)存,以及每次使用前可以立即從內(nèi)存刷新,即一個(gè)線程修改了某個(gè)變量的值,其它線程讀取的話肯定能看到新的值;

普通變量:

寫命中:當(dāng)處理器將操作數(shù)寫回到一個(gè)內(nèi)存緩存的區(qū)域時(shí),它首先會(huì)檢查這個(gè)緩存的內(nèi)存地址是否在緩存行中,如果不存在一個(gè)有效的緩存行,則處理器將這個(gè)操作數(shù)寫回到緩存,而不是寫回到內(nèi)存,這個(gè)操作被稱為寫命中。

術(shù)語 英文單詞 描述
共享變量 在多個(gè)線程之間能夠被共享的變量被稱為共享變量。共享變量包括所有的實(shí)例變量,靜態(tài)變量和數(shù)組元素。他們都被存放在堆內(nèi)存中,Volatile只作用于共享變量。
內(nèi)存屏障 Memory Barriers 是一組處理器指令,用于實(shí)現(xiàn)對(duì)內(nèi)存操作的順序限制。
緩沖行 Cache line 緩存中可以分配的最小存儲(chǔ)單位。處理器填寫緩存線時(shí)會(huì)加載整個(gè)緩存線,需要使用多個(gè)主內(nèi)存讀周期。
原子操作 Atomic operations 不可中斷的一個(gè)或一系列操作
緩存行填充 cache line fill 當(dāng)處理器識(shí)別到從內(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í),它首先會(huì)檢查這個(gè)緩存的內(nèi)存地址是否在緩存行中,如果不存在一個(gè)有效的緩存行,則處理器將這個(gè)操作數(shù)寫回到緩存,而不是寫回到內(nèi)存,這個(gè)操作被稱為寫命中。
寫缺失 write misses the cache 一個(gè)有效的緩存行被寫入到不存在的內(nèi)存區(qū)域。

單核CPU緩存結(jié)構(gòu)

單核CPU緩存


c500

多核CPU緩存


c500

所謂緩存行就是緩存中可以分配的最小存儲(chǔ)單位。

處理器填寫緩存行時(shí)會(huì)加載整個(gè)緩存行,需要使用多個(gè)主內(nèi)存讀周期。

下面講到偽緩存時(shí)會(huì)介紹,多核CPU、內(nèi)存的緩存系統(tǒng);

Information transfer between the cache and the memory is in terms of complete cache lines, rather than individual bytes. Thus if the program needs a particular byte, the entire cache line containing that byte is obtained from the memory. For example, suppose that the cache of Figure 2 was being used and the program fetches the word (two bytes) at location 0004736. If none of the cache lines contain the 16 bytes stored in addresses 0004730 through 000473F, then these 16 bytes are transferred from the memory to one of the cache lines. Because of the spatial locality of the program, we expect that other values in the cache line thus loaded will be referenced in the near future.

Volatile的實(shí)現(xiàn)原理

那么Volatile是如何來保證可見性的呢?在x86處理器下通過工具獲取JIT編譯器生成的匯編指令來看看對(duì)Volatile進(jìn)行寫操作CPU會(huì)做什么事情。

Java代碼

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

匯編代碼

0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: **lock** addl $0x0,(%esp);

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

  • 將當(dāng)前處理器緩存行的數(shù)據(jù)會(huì)寫回到系統(tǒng)內(nèi)存。
  • 這個(gè)寫回內(nèi)存的操作會(huì)引起在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效。

關(guān)鍵點(diǎn):其實(shí)相當(dāng)于線程 向緩存行寫數(shù)據(jù)的時(shí)候,會(huì)鎖住緩存行,是其他線程不能讀,寫完后失效緩存行,其他線程便可以從內(nèi)存讀到共享變量的最新值了;

深度解析:

處理器為了提高處理速度,不直接和內(nèi)存進(jìn)行通訊,而是先將系統(tǒng)內(nèi)存的數(shù)據(jù)讀到內(nèi)部緩存(L1,L2或其他)后再進(jìn)行操作,但操作完之后不知道何時(shí)會(huì)寫到內(nèi)存;如果對(duì)聲明了Volatile變量進(jìn)行寫操作,JVM就會(huì)向處理器發(fā)送一條Lock前綴的指令,將這個(gè)變量所在緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存。但是就算寫回到內(nèi)存,如果其他處理器緩存的值還是舊的,再執(zhí)行計(jì)算操作就會(huì)有問題,所以在多處理器下,為了保證各個(gè)處理器的緩存是一致的,就會(huì)實(shí)現(xiàn)緩存一致性協(xié)議每個(gè)處理器通過嗅探在總線上傳播的數(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ù)讀到處理器緩存里。

緩存一致性

Lock前綴指令會(huì)引起處理器緩存回寫到內(nèi)存。Lock前綴指令導(dǎo)致在執(zhí)行指令期間,聲言處理器的 LOCK# 信號(hào)。在多處理器環(huán)境中,LOCK# 信號(hào)確保在聲言該信號(hào)期間,處理器可以獨(dú)占使用任何共享內(nèi)存。(因?yàn)樗鼤?huì)鎖住總線,導(dǎo)致其他CPU不能訪問總線,不能訪問總線就意味著不能訪問系統(tǒng)內(nèi)存),但是在最近的處理器里,LOCK#信號(hào)一般不鎖總線,而是鎖緩存,畢竟鎖總線開銷比較大。在8.1.4章節(jié)有詳細(xì)說明鎖定操作對(duì)處理器緩存的影響,對(duì)于Intel486和Pentium處理器,在鎖操作時(shí),總是在總線上聲言LOCK#信號(hào)。但在P6和最近的處理器中,如果訪問的內(nèi)存區(qū)域已經(jīng)緩存在處理器內(nèi)部,則不會(huì)聲言LOCK#信號(hào)。相反地,它會(huì)鎖定這塊內(nèi)存區(qū)域的緩存并回寫到內(nèi)存,并使用緩存一致性機(jī)制來確保修改的原子性,此操作被稱為“緩存鎖定”,緩存一致性機(jī)制會(huì)阻止同時(shí)修改被兩個(gè)以上處理器緩存的內(nèi)存區(qū)域數(shù)據(jù)

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

為什么volatile不能保證原子性

讓一個(gè)volatile的integer自增(i++)其實(shí)要分成3步:

  1. 讀取volatile變量值到local;
  2. 增加變量的值;
  3. 把local的值寫回,讓其它的線程可見。這3步的jvm指令為:
mov 0xc (%r10),%r8d ; Load
inc    %r8d           ; Increment
mov    %r8d,0xc(%r10) ; Store
lock addl

注意最后一步是內(nèi)存屏障。

什么是內(nèi)存屏障(Memory Barrier)

內(nèi)存屏障的作用是防止CPU為了提升性能而進(jìn)行的亂序執(zhí)行

內(nèi)存屏障(memory barrier)是一個(gè)CPU指令。基本上,它是這樣一條指令:

  • 確保一些特定操作執(zhí)行的順序;

  • 告訴CPU和編譯器先于這個(gè)命令的必須先執(zhí)行,后于這個(gè)命令的必須后執(zhí)行

  • 內(nèi)存屏障另一個(gè)作用是強(qiáng)制更新一次不同CPU的緩存。例如:

    一個(gè)寫屏障會(huì)把這個(gè)屏障前寫入的數(shù)據(jù)刷新到各自核心的緩存,這樣任何核心中的線程試圖讀取該數(shù)據(jù)的線程將得到當(dāng)前核心緩存中的最新值

內(nèi)存屏障(memory barrier)和volatile什么關(guān)系?

上面的虛擬機(jī)指令里面有提到,如果你的字段是volatile,Java內(nèi)存模型將在寫操作后插入一個(gè)寫屏障指令,在讀操作前插入一個(gè)讀屏障指令

這意味著如果你對(duì)一個(gè)volatile字段進(jìn)行寫操作,你必須知道:

  • 在你寫入前,會(huì)保證所有之前發(fā)生的事已經(jīng)發(fā)生,并且任何更新過的數(shù)據(jù)值也是可見的,因?yàn)閮?nèi)存屏障會(huì)把之前的寫入值都刷新到緩存。
  • 一旦你完成寫入,任何訪問這個(gè)字段的線程將會(huì)得到最新的值。

不要將volatile用在getAndOperate場(chǎng)合,僅僅set或者get的場(chǎng)景是適合volatile的

不要將volatile用在getAndOperate場(chǎng)合(這種場(chǎng)合不原子,需要再加鎖),僅僅set或者get的場(chǎng)景是適合volatile的

Volatile的使用優(yōu)化

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

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

為什么追加64字節(jié)能夠提高并發(fā)編程的效率呢? 因?yàn)閷?duì)于英特爾酷睿i7,酷睿, Atom和NetBurst, Core Solo和Pentium M處理器的L1,L2或L3緩存的高速緩存行是64個(gè)字節(jié)寬,不支持部分填充緩存行,這意味著如果隊(duì)列的頭節(jié)點(diǎn)和尾節(jié)點(diǎn)都不足64字節(jié)的話,處理器會(huì)將它們都讀到同一個(gè)高速緩存行中,在多處理器下每個(gè)處理器都會(huì)緩存同樣的頭尾節(jié)點(diǎn),當(dāng)一個(gè)處理器試圖修改頭接點(diǎn)時(shí)會(huì)將整個(gè)緩存行鎖定,那么在緩存一致性機(jī)制的作用下,會(huì)導(dǎo)致其他處理器不能訪問自己高速緩存中的尾節(jié)點(diǎn),而隊(duì)列的入隊(duì)和出隊(duì)操作是需要不停修改頭接點(diǎn)和尾節(jié)點(diǎn),所以在多處理器的情況下將會(huì)嚴(yán)重影響到隊(duì)列的入隊(duì)和出隊(duì)效率。Doug lea使用追加到64字節(jié)的方式來填滿高速緩沖區(qū)的緩存行,避免頭接點(diǎn)和尾節(jié)點(diǎn)加載到同一個(gè)緩存行,使得頭尾節(jié)點(diǎn)在修改時(shí)不會(huì)互相鎖定。

那么是不是在使用Volatile變量時(shí)都應(yīng)該追加到64字節(jié)呢?不是的。在兩種場(chǎng)景下不應(yīng)該使用這種方式。第一:緩存行非64字節(jié)寬的處理器,如P6系列和奔騰處理器,它們的L1和L2高速緩存行是32個(gè)字節(jié)寬。第二:共享變量不會(huì)被頻繁的寫。因?yàn)槭褂米芳幼止?jié)的方式需要處理器讀取更多的字節(jié)到高速緩沖區(qū),這本身就會(huì)帶來一定的性能消耗,共享變量如果不被頻繁寫的話,鎖的幾率也非常小,就沒必要通過追加字節(jié)的方式來避免相互鎖定。

Java并發(fā)編程實(shí)踐寫道:“一個(gè)理解volatile變量好的方法:想想它們的行為與SynchrosizedInteger類相似,只不過用get和set方法取代了對(duì)volatile變量的讀寫操作。然而訪問volatile變量的操作不會(huì)加鎖,也就不會(huì)引起線程的阻塞,這使volatile相對(duì)于synchronized而言,只是輕量級(jí)的同步機(jī)制”

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評(píng)論 6 543
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,520評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,362評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,541評(píng)論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,896評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評(píng)論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,062評(píng)論 0 290
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,608評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,356評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,555評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評(píng)論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,769評(píng)論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評(píng)論 1 295
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,289評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,516評(píng)論 2 379

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