深入分析java并發(fā)編程中volatile的實(shí)現(xiàn)原理
引言
在多線程并發(fā)編程中synchronized和Volatile都扮演著重要的角色,Volatile是輕量級(jí)的synchronized,它在多處理器開(kāi)發(fā)中保證了共享變量的“可見(jiàn)性”??梢?jiàn)性的意思是當(dāng)一個(gè)線程修改一個(gè)共享變量時(shí),另外一個(gè)線程能讀到這個(gè)修改的值。它在某些情況下比synchronized的開(kāi)銷(xiāo)更小,本文將深入分析在硬件層面上Inter處理器是如何實(shí)現(xiàn)Volatile的,通過(guò)深入分析能幫助我們正確的使用Volatile變量。
術(shù)語(yǔ)定義
術(shù)語(yǔ) | 英文單詞 | 描述 |
共享變量 | 在多個(gè)線程之間能夠被共享的變量被稱(chēng)為共享變量。共享變量包括所有的實(shí)例變量,靜態(tài)變量和數(shù)組元素。他們都被存放在堆內(nèi)存中,Volatile只作用于共享變量。 | |
內(nèi)存屏障 | Memory Barriers | 是一組處理器指令,用于實(shí)現(xiàn)對(duì)內(nèi)存操作的順序限制。 |
緩沖行 | Cache line | 緩存中可以分配的最小存儲(chǔ)單位。處理器填寫(xiě)緩存線時(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)存位置仍然是下次處理器訪問(wèn)的地址時(shí),處理器從緩存中讀取操作數(shù),而不是從內(nèi)存。 |
寫(xiě)命中 | write hit | 當(dāng)處理器將操作數(shù)寫(xiě)回到一個(gè)內(nèi)存緩存的區(qū)域時(shí),它首先會(huì)檢查這個(gè)緩存的內(nèi)存地址是否在緩存行中,如果存在一個(gè)有效的緩存行,則處理器將這個(gè)操作數(shù)寫(xiě)回到緩存,而不是寫(xiě)回到內(nèi)存,這個(gè)操作被稱(chēng)為寫(xiě)命中。 |
寫(xiě)缺失 | write misses the cache | 一個(gè)有效的緩存行被寫(xiě)入到不存在的內(nèi)存區(qū)域。 |
Volatile的官方定義
Java語(yǔ)言規(guī)范第三版中對(duì)volatile的定義如下: java編程語(yǔ)言允許線程訪問(wèn)共享變量,為了確保共享變量能被準(zhǔn)確和一致的更新,線程應(yīng)該確保通過(guò)排他鎖單獨(dú)獲得這個(gè)變量。Java語(yǔ)言提供了volatile,在某些情況下比鎖更加方便。如果一個(gè)字段被聲明成volatile,java線程內(nèi)存模型確保所有線程看到這個(gè)變量的值是一致的。
為什么要使用Volatile
Volatile變量修飾符如果使用恰當(dāng)?shù)脑挘萻ynchronized的使用和執(zhí)行成本會(huì)更低,因?yàn)樗粫?huì)引起線程上下文的切換和調(diào)度。
Volatile的實(shí)現(xiàn)原理
那么Volatile是如何來(lái)保證可見(jiàn)性的呢?在x86處理器下通過(guò)工具獲取JIT編譯器生成的匯編指令來(lái)看看對(duì)Volatile進(jìn)行寫(xiě)操作CPU會(huì)做什么事情。
Java代碼: | instance = new Singleton();//instance是volatile變量 |
匯編代碼: | 0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp); |
有volatile變量修飾的共享變量進(jìn)行寫(xiě)操作的時(shí)候會(huì)多第二行匯編代碼,通過(guò)查IA-32架構(gòu)軟件開(kāi)發(fā)者手冊(cè)可知,lock前綴的指令在多核處理器下會(huì)引發(fā)了兩件事情。
將當(dāng)前處理器緩存行的數(shù)據(jù)會(huì)寫(xiě)回到系統(tǒng)內(nèi)存。
這個(gè)寫(xiě)回內(nèi)存的操作會(huì)引起在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無(wú)效。
處理器為了提高處理速度,不直接和內(nèi)存進(jìn)行通訊,而是先將系統(tǒng)內(nèi)存的數(shù)據(jù)讀到內(nèi)部緩存(L1,L2或其他)后再進(jìn)行操作,但操作完之后不知道何時(shí)會(huì)寫(xiě)到內(nèi)存,如果對(duì)聲明了Volatile變量進(jìn)行寫(xiě)操作,JVM就會(huì)向處理器發(fā)送一條Lock前綴的指令,將這個(gè)變量所在緩存行的數(shù)據(jù)寫(xiě)回到系統(tǒng)內(nèi)存。但是就算寫(xiě)回到內(nèi)存,如果其他處理器緩存的值還是舊的,再執(zhí)行計(jì)算操作就會(huì)有問(wèn)題,所以在多處理器下,為了保證各個(gè)處理器的緩存是一致的,就會(huì)實(shí)現(xiàn)緩存一致性協(xié)議,每個(gè)處理器通過(guò)嗅探在總線上傳播的數(shù)據(jù)來(lái)檢查自己緩存的值是不是過(guò)期了,當(dāng)處理器發(fā)現(xiàn)自己緩存行對(duì)應(yīng)的內(nèi)存地址被修改,就會(huì)將當(dāng)前處理器的緩存行設(shè)置成無(wú)效狀態(tài),當(dāng)處理器要對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作的時(shí)候,會(huì)強(qiáng)制重新從系統(tǒng)內(nèi)存里把數(shù)據(jù)讀到處理器緩存里。
這兩件事情在IA-32軟件開(kāi)發(fā)者架構(gòu)手冊(cè)的第三冊(cè)的多處理器管理章節(jié)(第八章)中有詳細(xì)闡述。
Lock前綴指令會(huì)引起處理器緩存回寫(xiě)到內(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不能訪問(wèn)總線,不能訪問(wèn)總線就意味著不能訪問(wèn)系統(tǒng)內(nèi)存),但是在最近的處理器里,LOCK#信號(hào)一般不鎖總線,而是鎖緩存,畢竟鎖總線開(kāi)銷(xiāo)比較大。在8.1.4章節(jié)有詳細(xì)說(shuō)明鎖定操作對(duì)處理器緩存的影響,對(duì)于Intel486和Pentium處理器,在鎖操作時(shí),總是在總線上聲言LOCK#信號(hào)。但在P6和最近的處理器中,如果訪問(wèn)的內(nèi)存區(qū)域已經(jīng)緩存在處理器內(nèi)部,則不會(huì)聲言LOCK#信號(hào)。相反地,它會(huì)鎖定這塊內(nèi)存區(qū)域的緩存并回寫(xiě)到內(nèi)存,并使用緩存一致性機(jī)制來(lái)確保修改的原子性,此操作被稱(chēng)為“緩存鎖定”,緩存一致性機(jī)制會(huì)阻止同時(shí)修改被兩個(gè)以上處理器緩存的內(nèi)存區(qū)域數(shù)據(jù)。
一個(gè)處理器的緩存回寫(xiě)到內(nèi)存會(huì)導(dǎo)致其他處理器的緩存無(wú)效。IA-32處理器和Intel 64處理器使用MESI(修改,獨(dú)占,共享,無(wú)效)控制協(xié)議去維護(hù)內(nèi)部緩存和其他處理器緩存的一致性。在多核處理器系統(tǒng)中進(jìn)行操作的時(shí)候,IA-32 和Intel 64處理器能嗅探其他處理器訪問(wèn)系統(tǒng)內(nèi)存和它們的內(nèi)部緩存。它們使用嗅探技術(shù)保證它的內(nèi)部緩存,系統(tǒng)內(nèi)存和其他處理器的緩存的數(shù)據(jù)在總線上保持一致。例如在Pentium和P6 family處理器中,如果通過(guò)嗅探一個(gè)處理器來(lái)檢測(cè)其他處理器打算寫(xiě)內(nèi)存地址,而這個(gè)地址當(dāng)前處理共享狀態(tài),那么正在嗅探的處理器將無(wú)效它的緩存行,在下次訪問(wèn)相同內(nèi)存地址時(shí),強(qiáng)制執(zhí)行緩存行填充。
Volatile的使用優(yōu)化
著名的Java并發(fā)編程大師Doug lea在JDK7的并發(fā)包里新增一個(gè)隊(duì)列集合類(lèi)LinkedTransferQueue,他在使用Volatile變量時(shí),用一種追加字節(jié)的方式來(lái)優(yōu)化隊(duì)列出隊(duì)和入隊(duì)的性能。
追加字節(jié)能優(yōu)化性能?這種方式看起來(lái)很神奇,但如果深入理解處理器架構(gòu)就能理解其中的奧秘。讓我們先來(lái)看看LinkedTransferQueue這個(gè)類(lèi),它使用一個(gè)內(nèi)部類(lèi)類(lèi)型來(lái)定義隊(duì)列的頭隊(duì)列(Head)和尾節(jié)點(diǎn)(tail),而這個(gè)內(nèi)部類(lèi)PaddedAtomicReference相對(duì)于父類(lèi)AtomicReference只做了一件事情,就將共享變量追加到64字節(jié)。我們可以來(lái)計(jì)算下,一個(gè)對(duì)象的引用占4個(gè)字節(jié),它追加了15個(gè)變量共占60個(gè)字節(jié),再加上父類(lèi)的Value變量,一共64個(gè)字節(jié)。
/** head of the queue */ private transient final PaddedAtomicReference<QNode> head; /** tail of the queue */ private transient final PaddedAtomicReference<QNode> tail; static final class PaddedAtomicReference <T> extends AtomicReference <T> { // enough padding for 64bytes with 4byte refs 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; //省略其他代碼 }
為什么追加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)致其他處理器不能訪問(wèn)自己高速緩存中的尾節(jié)點(diǎn),而隊(duì)列的入隊(duì)和出隊(duì)操作是需要不停修改頭接點(diǎn)和尾節(jié)點(diǎn),所以在多處理器的情況下將會(huì)嚴(yán)重影響到隊(duì)列的入隊(duì)和出隊(duì)效率。Doug lea使用追加到64字節(jié)的方式來(lái)填滿高速緩沖區(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ì)被頻繁的寫(xiě)。因?yàn)槭褂米芳幼止?jié)的方式需要處理器讀取更多的字節(jié)到高速緩沖區(qū),這本身就會(huì)帶來(lái)一定的性能消耗,共享變量如果不被頻繁寫(xiě)的話,鎖的幾率也非常小,就沒(méi)必要通過(guò)追加字節(jié)的方式來(lái)避免相互鎖定。
總結(jié)
以上就是本文關(guān)于深入分析java并發(fā)編程中Volatile的實(shí)現(xiàn)原理的全部?jī)?nèi)容,希望對(duì)大家有所幫助。感興趣的朋友歡迎繼續(xù)參閱本站:
Java并發(fā)實(shí)例之CyclicBarrier的使用
java并發(fā)學(xué)習(xí)之BlockingQueue實(shí)現(xiàn)生產(chǎn)者消費(fèi)者詳解
如有不足之處,歡迎留言指出。感謝朋友們對(duì)本站的支持!
- Java并發(fā)編程——volatile關(guān)鍵字
- Java并發(fā)教程之volatile關(guān)鍵字詳解
- 詳解java并發(fā)編程(2) --Synchronized與Volatile區(qū)別
- Java并發(fā)編程-volatile可見(jiàn)性詳解
- Java多線程并發(fā)編程 Volatile關(guān)鍵字
- java 高并發(fā)中volatile的實(shí)現(xiàn)原理
- Java并發(fā)編程:volatile關(guān)鍵字詳細(xì)解析
- Java 并發(fā)編程:volatile的使用及其原理解析
- Java并發(fā)編程之volatile變量介紹
- 詳解Java并發(fā)編程基礎(chǔ)之volatile
相關(guān)文章
Java線程池運(yùn)行狀態(tài)監(jiān)控實(shí)現(xiàn)解析
這篇文章主要介紹了Java線程池運(yùn)行狀態(tài)監(jiān)控實(shí)現(xiàn)解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-10-10SpringBoot中利用AOP和攔截器實(shí)現(xiàn)自定義注解
本文將通過(guò)攔截器+AOP實(shí)現(xiàn)自定義注解,在這里攔截器充當(dāng)在指定注解處要執(zhí)行的方法,aop負(fù)責(zé)將攔截器的方法和要注解生效的地方做一個(gè)織入,感興趣的可以嘗試一下2022-06-06javaweb項(xiàng)目如何實(shí)現(xiàn)手機(jī)短信登錄
這篇文章主要介紹了javaweb項(xiàng)目如何實(shí)現(xiàn)手機(jī)短信登錄,手機(jī)號(hào)登錄在現(xiàn)在的項(xiàng)目中用的場(chǎng)景非常多,實(shí)現(xiàn)起來(lái)也不難,今天我們就一起來(lái)通過(guò)演示實(shí)現(xiàn)登錄過(guò)程,需要的朋友可以參考下2019-07-07idea debug沒(méi)有force step into的問(wèn)題解決
本文主要介紹了IDEA Debug中ForceStepInto按鈕消失的問(wèn)題及解決方法,文中通過(guò)圖文介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-10-10MyBatis的9種動(dòng)態(tài)標(biāo)簽詳解
大家好,本篇文章主要講的是MyBatis的9種動(dòng)態(tài)標(biāo)簽詳解,感興趣的同學(xué)趕快來(lái)看一看吧,感興趣的同學(xué)趕快來(lái)看一看吧2021-12-12java反射_改變private中的變量及方法的簡(jiǎn)單實(shí)例
下面小編就為大家?guī)?lái)一篇java反射_改變private中的變量及方法的簡(jiǎn)單實(shí)例。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-06-06