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ā)了兩件事情误债。
將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存。
這個(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)原則俺泣。
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ù)厕九。
一個(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)該使用這種方式:
- 緩存行非64字節(jié)寬的處理器。如P6系列和奔騰處理器像街,它們的L1和L2告訴緩存行是32個(gè)字節(jié)寬黎棠。
- 共享變量不會被頻繁地寫。因?yàn)槭褂米芳幼止?jié)地方式需要處理器讀取更多的字節(jié)到高速緩沖區(qū)镰绎,這本身就會帶來一定的性能消耗脓斩,如果共享變量不被頻繁寫的話,鎖的幾率也非常小畴栖,就沒必要通過追加字節(jié)的方式來避免相互鎖定随静。