volatile關(guān)鍵字
當(dāng)變量被某個線程A修改值之后,其它線程比如B若讀取此變量的話,立刻可以看到原來線程A修改后的值
注:普通變量與volatile變量的區(qū)別是volatile的特殊規(guī)則保證了新值能立即同步到主內(nèi)存,以及每次使用前可以立即從內(nèi)存刷新,即一個線程修改了某個變量的值,其它線程讀取的話肯定能看到新的值;
普通變量:
寫命中:當(dāng)處理器將操作數(shù)寫回到一個內(nèi)存緩存的區(qū)域時,它首先會檢查這個緩存的內(nèi)存地址是否在緩存行中,如果不存在一個有效的緩存行,則處理器將這個操作數(shù)寫回到緩存,而不是寫回到內(nèi)存,這個操作被稱為寫命中。
術(shù)語 | 英文單詞 | 描述 |
---|---|---|
共享變量 | 在多個線程之間能夠被共享的變量被稱為共享變量。共享變量包括所有的實例變量,靜態(tài)變量和數(shù)組元素。他們都被存放在堆內(nèi)存中,Volatile只作用于共享變量。 | |
內(nèi)存屏障 | Memory Barriers | 是一組處理器指令,用于實現(xiàn)對內(nèi)存操作的順序限制。 |
緩沖行 | Cache line | 緩存中可以分配的最小存儲單位。處理器填寫緩存線時會加載整個緩存線,需要使用多個主內(nèi)存讀周期。 |
原子操作 | Atomic operations | 不可中斷的一個或一系列操作 |
緩存行填充 | cache line fill | 當(dāng)處理器識別到從內(nèi)存中讀取操作數(shù)是可緩存的,處理器讀取整個緩存行到適當(dāng)?shù)木彺妫↙1,L2,L3的或所有) |
緩存命中 | cache hit | 如果進(jìn)行高速緩存行填充操作的內(nèi)存位置仍然是下次處理器訪問的地址時,處理器從緩存中讀取操作數(shù),而不是從內(nèi)存。 |
寫命中 | write hit | 當(dāng)處理器將操作數(shù)寫回到一個內(nèi)存緩存的區(qū)域時,它首先會檢查這個緩存的內(nèi)存地址是否在緩存行中,如果不存在一個有效的緩存行,則處理器將這個操作數(shù)寫回到緩存,而不是寫回到內(nèi)存,這個操作被稱為寫命中。 |
寫缺失 | write misses the cache | 一個有效的緩存行被寫入到不存在的內(nèi)存區(qū)域。 |
單核CPU緩存結(jié)構(gòu)
單核CPU緩存
多核CPU緩存
所謂緩存行就是緩存中可以分配的最小存儲單位。
處理器填寫緩存行時會加載整個緩存行,需要使用多個主內(nèi)存讀周期。
下面講到偽緩存時會介紹,多核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的實現(xiàn)原理
那么Volatile是如何來保證可見性的呢?在x86處理器下通過工具獲取JIT編譯器生成的匯編指令來看看對Volatile進(jìn)行寫操作CPU會做什么事情。
Java代碼
instance = new Singleton();//instance是volatile變量
匯編代碼
0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: **lock** addl $0x0,(%esp);
有volatile變量修飾的共享變量進(jìn)行寫操作的時候會多第二行匯編代碼,通過查IA-32架構(gòu)軟件開發(fā)者手冊可知,lock前綴的指令在多核處理器下會引發(fā)了兩件事情
- 將當(dāng)前處理器緩存行的數(shù)據(jù)會寫回到系統(tǒng)內(nèi)存。
- 這個寫回內(nèi)存的操作會引起在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效。
關(guān)鍵點:其實相當(dāng)于線程 向緩存行寫數(shù)據(jù)的時候,會鎖住緩存行,是其他線程不能讀,寫完后失效緩存行,其他線程便可以從內(nèi)存讀到共享變量的最新值了;
深度解析:
處理器為了提高處理速度,不直接和內(nèi)存進(jìn)行通訊,而是先將系統(tǒng)內(nèi)存的數(shù)據(jù)讀到內(nèi)部緩存(L1,L2或其他)后再進(jìn)行操作,但操作完之后不知道何時會寫到內(nèi)存;如果對聲明了Volatile變量進(jìn)行寫操作,JVM就會向處理器發(fā)送一條Lock前綴的指令,將這個變量所在緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存。但是就算寫回到內(nèi)存,如果其他處理器緩存的值還是舊的,再執(zhí)行計算操作就會有問題,所以在多處理器下,為了保證各個處理器的緩存是一致的,就會實現(xiàn)緩存一致性協(xié)議,每個處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己緩存的值是不是過期了,當(dāng)處理器發(fā)現(xiàn)自己緩存行對應(yīng)的內(nèi)存地址被修改,就會將當(dāng)前處理器的緩存行設(shè)置成無效狀態(tài),當(dāng)處理器要對這個數(shù)據(jù)進(jìn)行修改操作的時候,會強(qiáng)制重新從系統(tǒng)內(nèi)存里把數(shù)據(jù)讀到處理器緩存里。
Lock前綴指令會引起處理器緩存回寫到內(nèi)存。Lock前綴指令導(dǎo)致在執(zhí)行指令期間,聲言處理器的 LOCK# 信號。在多處理器環(huán)境中,LOCK# 信號確保在聲言該信號期間,處理器可以獨占使用任何共享內(nèi)存。(因為它會鎖住總線,導(dǎo)致其他CPU不能訪問總線,不能訪問總線就意味著不能訪問系統(tǒng)內(nèi)存),但是在最近的處理器里,LOCK#信號一般不鎖總線,而是鎖緩存,畢竟鎖總線開銷比較大。在8.1.4章節(jié)有詳細(xì)說明鎖定操作對處理器緩存的影響,對于Intel486和Pentium處理器,在鎖操作時,總是在總線上聲言LOCK#信號。但在P6和最近的處理器中,如果訪問的內(nèi)存區(qū)域已經(jīng)緩存在處理器內(nèi)部,則不會聲言LOCK#信號。相反地,它會鎖定這塊內(nèi)存區(qū)域的緩存并回寫到內(nèi)存,并使用緩存一致性機(jī)制來確保修改的原子性,此操作被稱為“緩存鎖定”,緩存一致性機(jī)制會阻止同時修改被兩個以上處理器緩存的內(nèi)存區(qū)域數(shù)據(jù)。
一個處理器的緩存回寫到內(nèi)存會導(dǎo)致其他處理器的緩存無效。IA-32處理器和Intel 64處理器使用MESI(修改,獨占,共享,無效)控制協(xié)議去維護(hù)內(nèi)部緩存和其他處理器緩存的一致性。在多核處理器系統(tǒng)中進(jìn)行操作的時候,IA-32 和Intel 64處理器能嗅探其他處理器訪問系統(tǒng)內(nèi)存和它們的內(nèi)部緩存。它們使用嗅探技術(shù)保證它的內(nèi)部緩存,系統(tǒng)內(nèi)存和其他處理器的緩存的數(shù)據(jù)在總線上保持一致。例如在Pentium和P6 family處理器中,如果通過嗅探一個處理器來檢測其他處理器打算寫內(nèi)存地址,而這個地址當(dāng)前處理共享狀態(tài),那么正在嗅探的處理器將無效它的緩存行,在下次訪問相同內(nèi)存地址時,強(qiáng)制執(zhí)行緩存行填充。
為什么volatile不能保證原子性
讓一個volatile的integer自增(i++)其實要分成3步:
- 讀取volatile變量值到local;
- 增加變量的值;
- 把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)是一個CPU指令。基本上,它是這樣一條指令:
確保一些特定操作執(zhí)行的順序;
告訴CPU和編譯器先于這個命令的必須先執(zhí)行,后于這個命令的必須后執(zhí)行
-
內(nèi)存屏障另一個作用是強(qiáng)制更新一次不同CPU的緩存。例如:
一個寫屏障會把這個屏障前寫入的數(shù)據(jù)刷新到各自核心的緩存,這樣任何核心中的線程試圖讀取該數(shù)據(jù)的線程將得到當(dāng)前核心緩存中的最新值
內(nèi)存屏障(memory barrier)和volatile什么關(guān)系?
上面的虛擬機(jī)指令里面有提到,如果你的字段是volatile,Java內(nèi)存模型將在寫操作后插入一個寫屏障指令,在讀操作前插入一個讀屏障指令。
這意味著如果你對一個volatile字段進(jìn)行寫操作,你必須知道:
- 在你寫入前,會保證所有之前發(fā)生的事已經(jīng)發(fā)生,并且任何更新過的數(shù)據(jù)值也是可見的,因為內(nèi)存屏障會把之前的寫入值都刷新到緩存。
- 一旦你完成寫入,任何訪問這個字段的線程將會得到最新的值。
不要將volatile用在getAndOperate場合,僅僅set或者get的場景是適合volatile的
不要將volatile用在getAndOperate場合(這種場合不原子,需要再加鎖),僅僅set或者get的場景是適合volatile的。
Volatile的使用優(yōu)化
著名的Java并發(fā)編程大師Doug lea在JDK7的并發(fā)包里新增一個隊列集合類LinkedTransferQueue,他在使用Volatile變量時,用一種追加字節(jié)的方式來優(yōu)化隊列出隊和入隊的性能。
追加字節(jié)能優(yōu)化性能?這種方式看起來很神奇,但如果深入理解處理器架構(gòu)就能理解其中的奧秘。讓我們先來看看LinkedTransferQueue這個類,它使用一個內(nèi)部類類型來定義隊列的頭隊列(Head)和尾節(jié)點(tail),而這個內(nèi)部類PaddedAtomicReference相對于父類AtomicReference只做了一件事情,就將共享變量追加到64字節(jié)。我們可以來計算下,一個對象的引用占4個字節(jié),它追加了15個變量共占60個字節(jié),再加上父類的Value變量,一共64個字節(jié)。
為什么追加64字節(jié)能夠提高并發(fā)編程的效率呢? 因為對于英特爾酷睿i7,酷睿, Atom和NetBurst, Core Solo和Pentium M處理器的L1,L2或L3緩存的高速緩存行是64個字節(jié)寬,不支持部分填充緩存行,這意味著如果隊列的頭節(jié)點和尾節(jié)點都不足64字節(jié)的話,處理器會將它們都讀到同一個高速緩存行中,在多處理器下每個處理器都會緩存同樣的頭尾節(jié)點,當(dāng)一個處理器試圖修改頭接點時會將整個緩存行鎖定,那么在緩存一致性機(jī)制的作用下,會導(dǎo)致其他處理器不能訪問自己高速緩存中的尾節(jié)點,而隊列的入隊和出隊操作是需要不停修改頭接點和尾節(jié)點,所以在多處理器的情況下將會嚴(yán)重影響到隊列的入隊和出隊效率。Doug lea使用追加到64字節(jié)的方式來填滿高速緩沖區(qū)的緩存行,避免頭接點和尾節(jié)點加載到同一個緩存行,使得頭尾節(jié)點在修改時不會互相鎖定。
那么是不是在使用Volatile變量時都應(yīng)該追加到64字節(jié)呢?不是的。在兩種場景下不應(yīng)該使用這種方式。第一:緩存行非64字節(jié)寬的處理器,如P6系列和奔騰處理器,它們的L1和L2高速緩存行是32個字節(jié)寬。第二:共享變量不會被頻繁的寫。因為使用追加字節(jié)的方式需要處理器讀取更多的字節(jié)到高速緩沖區(qū),這本身就會帶來一定的性能消耗,共享變量如果不被頻繁寫的話,鎖的幾率也非常小,就沒必要通過追加字節(jié)的方式來避免相互鎖定。
Java并發(fā)編程實踐寫道:“一個理解volatile變量好的方法:想想它們的行為與SynchrosizedInteger類相似,只不過用get和set方法取代了對volatile變量的讀寫操作。然而訪問volatile變量的操作不會加鎖,也就不會引起線程的阻塞,這使volatile相對于synchronized而言,只是輕量級的同步機(jī)制”