日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > java >内容正文

java

就是要你懂 Java 中 volatile 关键字实现原理

發(fā)布時間:2023/12/19 java 78 豆豆
生活随笔 收集整理的這篇文章主要介紹了 就是要你懂 Java 中 volatile 关键字实现原理 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

前言

我們知道volatile關(guān)鍵字的作用是保證變量在多線程之間的可見性,它是java.util.concurrent包的核心,沒有volatile就沒有這么多的并發(fā)類給我們使用。

本文詳細解讀一下volatile關(guān)鍵字如何保證變量在多線程之間的可見性,在此之前,有必要講解一下CPU緩存的相關(guān)知識,掌握這部分知識一定會讓我們更好地理解volatile的原理,從而更好、更正確地地使用volatile關(guān)鍵字。

CPU緩存

CPU緩存的出現(xiàn)主要是為了解決CPU運算速度與內(nèi)存讀寫速度不匹配的矛盾,因為CPU運算速度要比內(nèi)存讀寫速度快得多,舉個例子:

  • 一次主內(nèi)存的訪問通常在幾十到幾百個時鐘周期
  • 一次L1高速緩存的讀寫只需要1~2個時鐘周期
  • 一次L2高速緩存的讀寫也只需要數(shù)十個時鐘周期

這種訪問速度的顯著差異,導(dǎo)致CPU可能會花費很長時間等待數(shù)據(jù)到來或把數(shù)據(jù)寫入內(nèi)存。

基于此,現(xiàn)在CPU大多數(shù)情況下讀寫都不會直接訪問內(nèi)存(CPU都沒有連接到內(nèi)存的管腳),取而代之的是CPU緩存,CPU緩存是位于CPU與內(nèi)存之間的臨時存儲器,它的容量比內(nèi)存小得多但是交換速度卻比內(nèi)存快得多。而緩存中的數(shù)據(jù)是內(nèi)存中的一小部分數(shù)據(jù),但這一小部分是短時間內(nèi)CPU即將訪問的,當(dāng)CPU調(diào)用大量數(shù)據(jù)時,就可先從緩存中讀取,從而加快讀取速度。

按照讀取順序與CPU結(jié)合的緊密程度,CPU緩存可分為:

  • 一級緩存:簡稱L1 Cache,位于CPU內(nèi)核的旁邊,是與CPU結(jié)合最為緊密的CPU緩存
  • 二級緩存:簡稱L2 Cache,分內(nèi)部和外部兩種芯片,內(nèi)部芯片二級緩存運行速度與主頻相同,外部芯片二級緩存運行速度則只有主頻的一半
  • 三級緩存:簡稱L3 Cache,部分高端CPU才有

每一級緩存中所存儲的數(shù)據(jù)全部都是下一級緩存中的一部分,這三種緩存的技術(shù)難度和制造成本是相對遞減的,所以其容量也相對遞增。

當(dāng)CPU要讀取一個數(shù)據(jù)時,首先從一級緩存中查找,如果沒有再從二級緩存中查找,如果還是沒有再從三級緩存中或內(nèi)存中查找。一般來說每級緩存的命中率大概都有80%左右,也就是說全部數(shù)據(jù)量的80%都可以在一級緩存中找到,只剩下20%的總數(shù)據(jù)量才需要從二級緩存、三級緩存或內(nèi)存中讀取。

使用CPU緩存帶來的問題

用一張圖表示一下CPU–>CPU緩存–>主內(nèi)存數(shù)據(jù)讀取之間的關(guān)系:

當(dāng)系統(tǒng)運行時,CPU執(zhí)行計算的過程如下:

  • 程序以及數(shù)據(jù)被加載到主內(nèi)存
  • 指令和數(shù)據(jù)被加載到CPU緩存
  • CPU執(zhí)行指令,把結(jié)果寫到高速緩存
  • 高速緩存中的數(shù)據(jù)寫回主內(nèi)存
  • 如果服務(wù)器是單核CPU,那么這些步驟不會有任何的問題,但是如果服務(wù)器是多核CPU,那么問題來了,以Intel Core i7處理器的高速緩存概念模型為例(圖片摘自《深入理解計算機系統(tǒng)》):

    試想下面一種情況:

  • 核0讀取了一個字節(jié),根據(jù)局部性原理,它相鄰的字節(jié)同樣被被讀入核0的緩存
  • 核3做了上面同樣的工作,這樣核0與核3的緩存擁有同樣的數(shù)據(jù)
  • 核0修改了那個字節(jié),被修改后,那個字節(jié)被寫回核0的緩存,但是該信息并沒有寫回主存
  • 核3訪問該字節(jié),由于核0并未將數(shù)據(jù)寫回主存,數(shù)據(jù)不同步
  • 為了解決這個問題,CPU制造商制定了一個規(guī)則:當(dāng)一個CPU修改緩存中的字節(jié)時,服務(wù)器中其他CPU會被通知,它們的緩存將視為無效。于是,在上面的情況下,核3發(fā)現(xiàn)自己的緩存中數(shù)據(jù)已無效,核0將立即把自己的數(shù)據(jù)寫回主存,然后核3重新讀取該數(shù)據(jù)。

    反匯編Java字節(jié)碼,查看匯編層面對volatile關(guān)鍵字做了什么

    有了上面的理論基礎(chǔ),我們可以研究volatile關(guān)鍵字到底是如何實現(xiàn)的。首先寫一段簡單的代碼:

    1234567891011121314151617181920/**?* @author 五月的倉頡http://www.cnblogs.com/xrq730/p/7048693.html?*/public class LazySingleton {????private static volatile LazySingleton instance = null;?????????public static LazySingleton getInstance() {????????if (instance == null) {????????????instance = new LazySingleton();????????}?????????????????return instance;????}?????????public static void main(String[] args) {????????LazySingleton.getInstance();????}?????}

    首先反編譯一下這段代碼的.class文件,看一下生成的字節(jié)碼:

    沒有任何特別的。要知道,字節(jié)碼指令,比如上圖的getstatic、ifnonnull、new等,最終對應(yīng)到操作系統(tǒng)的層面,都是轉(zhuǎn)換為一條一條指令去執(zhí)行,我們使用的PC機、應(yīng)用服務(wù)器的CPU架構(gòu)通常都是IA-32架構(gòu)的,這種架構(gòu)采用的指令集是CISC(復(fù)雜指令集),而匯編語言則是這種指令集的助記符。

    因此,既然在字節(jié)碼層面我們看不出什么端倪,那下面就看看將代碼轉(zhuǎn)換為匯編指令能看出什么端倪。Windows上要看到以上代碼對應(yīng)的匯編碼不難(吐槽一句,說說不難,為了這個問題我找遍了各種資料,差點就準備安裝虛擬機,在Linux系統(tǒng)上搞了),訪問hsdis工具路徑可直接下載hsdis工具,下載完畢之后解壓,將hsdis-amd64.dll與hsdis-amd64.lib兩個文件放在%JAVA_HOME%\jre\bin\server路徑下即可,如下圖:

    然后跑main函數(shù),跑main函數(shù)之前,加入如下虛擬機參數(shù):

    1-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*LazySingleton.getInstance

    運行main函數(shù)即可,代碼生成的匯編指令為:

    123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687Java HotSpot(TM) 64-Bit Server VM warning: PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional outputCompilerOracle: compileonly *LazySingleton.getInstanceLoaded disassembler from D:\JDK\jre\bin\server\hsdis-amd64.dllDecoding compiled method 0x0000000002931150:Code:Argument 0 is unknown.RIP: 0x29312a0 Code size: 0x00000108[Disassembling for mach='amd64'][Entry Point][Verified Entry Point][Constants]??# {method} 'getInstance' '()Lorg/xrq/test/design/singleton/LazySingleton;' in 'org/xrq/test/design/singleton/LazySingleton'??#?????????? [sp+0x20]? (sp of caller)??0x00000000029312a0: mov???? dword ptr [rsp+0ffffffffffffa000h],eax??0x00000000029312a7: push??? rbp??0x00000000029312a8: sub???? rsp,10h?????????? ;*synchronization entry????????????????????????????????????????????????; - org.xrq.test.design.singleton.LazySingleton::getInstance@-1 (line 13)??0x00000000029312ac: mov???? r10,7ada9e428h??? ;?? {oop(a 'java/lang/Class' = 'org/xrq/test/design/singleton/LazySingleton')}??0x00000000029312b6: mov???? r11d,dword ptr [r10+58h]????????????????????????????????????????????????;*getstatic instance????????????????????????????????????????????????; - org.xrq.test.design.singleton.LazySingleton::getInstance@0 (line 13)??0x00000000029312ba: test??? r11d,r11d??0x00000000029312bd: je????? 29312e0h??0x00000000029312bf: mov???? r10,7ada9e428h??? ;?? {oop(a 'java/lang/Class' = 'org/xrq/test/design/singleton/LazySingleton')}??0x00000000029312c9: mov???? r11d,dword ptr [r10+58h]??0x00000000029312cd: mov???? rax,r11??0x00000000029312d0: shl???? rax,3h??????????? ;*getstatic instance????????????????????????????????????????????????; - org.xrq.test.design.singleton.LazySingleton::getInstance@16 (line 17)??0x00000000029312d4: add???? rsp,10h??0x00000000029312d8: pop???? rbp??0x00000000029312d9: test??? dword ptr [330000h],eax? ;?? {poll_return}??0x00000000029312df: ret??0x00000000029312e0: mov???? rax,qword ptr [r15+60h]??0x00000000029312e4: mov???? r10,rax??0x00000000029312e7: add???? r10,10h??0x00000000029312eb: cmp???? r10,qword ptr [r15+70h]??0x00000000029312ef: jnb???? 293135bh??0x00000000029312f1: mov???? qword ptr [r15+60h],r10??0x00000000029312f5: prefetchnta byte ptr [r10+0c0h]??0x00000000029312fd: mov???? r11d,0e07d00b2h?? ;?? {oop('org/xrq/test/design/singleton/LazySingleton')}??0x0000000002931303: mov???? r10,qword ptr [r12+r11*8+0b0h]??0x000000000293130b: mov???? qword ptr [rax],r10??0x000000000293130e: mov???? dword ptr [rax+8h],0e07d00b2h????????????????????????????????????????????????;?? {oop('org/xrq/test/design/singleton/LazySingleton')}??0x0000000002931315: mov???? dword ptr [rax+0ch],r12d??0x0000000002931319: mov???? rbp,rax?????????? ;*new? ; - org.xrq.test.design.singleton.LazySingleton::getInstance@6 (line 14)??0x000000000293131c: mov???? rdx,rbp??0x000000000293131f: call??? 2907c60h????????? ; OopMap{rbp=Oop off=132}????????????????????????????????????????????????;*invokespecial <init>????????????????????????????????????????????????; - org.xrq.test.design.singleton.LazySingleton::getInstance@10 (line 14)????????????????????????????????????????????????;?? {optimized virtual_call}??0x0000000002931324: mov???? r10,rbp??0x0000000002931327: shr???? r10,3h??0x000000000293132b: mov???? r11,7ada9e428h??? ;?? {oop(a 'java/lang/Class' = 'org/xrq/test/design/singleton/LazySingleton')}??0x0000000002931335: mov???? dword ptr [r11+58h],r10d??0x0000000002931339: mov???? r10,7ada9e428h??? ;?? {oop(a 'java/lang/Class' = 'org/xrq/test/design/singleton/LazySingleton')}??0x0000000002931343: shr???? r10,9h??0x0000000002931347: mov???? r11d,20b2000h??0x000000000293134d: mov???? byte ptr [r11+r10],r12l??0x0000000002931351: lock add dword ptr [rsp],0h? ;*putstatic instance????????????????????????????????????????????????; - org.xrq.test.design.singleton.LazySingleton::getInstance@13 (line 14)??0x0000000002931356: jmp???? 29312bfh??0x000000000293135b: mov???? rdx,703e80590h??? ;?? {oop('org/xrq/test/design/singleton/LazySingleton')}??0x0000000002931365: nop??0x0000000002931367: call??? 292fbe0h????????? ; OopMap{off=204}????????????????????????????????????????????????;*new? ; - org.xrq.test.design.singleton.LazySingleton::getInstance@6 (line 14)????????????????????????????????????????????????;?? {runtime_call}??0x000000000293136c: jmp???? 2931319h??0x000000000293136e: mov???? rdx,rax??0x0000000002931371: jmp???? 2931376h??0x0000000002931373: mov???? rdx,rax?????????? ;*new? ; - org.xrq.test.design.singleton.LazySingleton::getInstance@6 (line 14)??0x0000000002931376: add???? rsp,10h??0x000000000293137a: pop???? rbp??0x000000000293137b: jmp???? 2932b20h????????? ;?? {runtime_call}[Stub Code]??0x0000000002931380: mov???? rbx,0h??????????? ;?? {no_reloc}??0x000000000293138a: jmp???? 293138ah????????? ;?? {runtime_call}[Exception Handler]??0x000000000293138f: jmp???? 292fca0h????????? ;?? {runtime_call}[Deopt Handler Code]??0x0000000002931394: call??? 2931399h??0x0000000002931399: sub???? qword ptr [rsp],5h??0x000000000293139e: jmp???? 2909000h????????? ;?? {runtime_call}??0x00000000029313a3: hlt??0x00000000029313a4: hlt??0x00000000029313a5: hlt??0x00000000029313a6: hlt??0x00000000029313a7: hlt

    這么長長的匯編代碼,可能大家不知道CPU在哪里做了手腳,沒事不難,定位到59、60兩行:

    120x0000000002931351: lock add dword ptr [rsp],0h ;*putstatic instance; - org.xrq.test.design.singleton.LazySingleton::getInstance@13 (line 14)

    之所以定位到這兩行是因為這里結(jié)尾寫明了line 14,line 14即volatile變量instance賦值的地方。后面的add dword ptr [rsp],0h都是正常的匯編語句,意思是將雙字節(jié)的棧指針寄存器+0,這里的關(guān)鍵就是add前面的lock指令,后面詳細分析一下lock指令的作用和為什么加上lock指令后就能保證volatile關(guān)鍵字的內(nèi)存可見性。

    lock指令做了什么

    之前有說過IA-32架構(gòu),關(guān)于CPU架構(gòu)的問題大家有興趣的可以自己查詢一下,這里查詢一下IA-32手冊關(guān)于lock指令的描述,沒有IA-32手冊的可以去這個地址下載IA-32手冊下載地址,是個中文版本的手冊。

    我摘抄一下IA-32手冊中關(guān)于lock指令作用的一些描述(因為lock指令的作用在手冊中散落在各處,并不是在某一章或者某一節(jié)專門講):

    在修改內(nèi)存操作時,使用LOCK前綴去調(diào)用加鎖的讀-修改-寫操作,這種機制用于多處理器系統(tǒng)中處理器之間進行可靠的通訊,具體描述如下:
    (1)在Pentium和早期的IA-32處理器中,LOCK前綴會使處理器執(zhí)行當(dāng)前指令時產(chǎn)生一個LOCK#信號,這種總是引起顯式總線鎖定出現(xiàn)
    (2)在Pentium4、Inter Xeon和P6系列處理器中,加鎖操作是由高速緩存鎖或總線鎖來處理。如果內(nèi)存訪問有高速緩存且只影響一個單獨的高速緩存行,那么操作中就會調(diào)用高速緩存鎖,而系統(tǒng)總線和系統(tǒng)內(nèi)存中的實際區(qū)域內(nèi)不會被鎖定。同時,這條總線上的其它Pentium4、Intel Xeon或者P6系列處理器就回寫所有已修改的數(shù)據(jù)并使它們的高速緩存失效,以保證系統(tǒng)內(nèi)存的一致性。如果內(nèi)存訪問沒有高速緩存且/或它跨越了高速緩存行的邊界,那么這個處理器就會產(chǎn)生LOCK#信號,并在鎖定操作期間不會響應(yīng)總線控制請求

    32位IA-32處理器支持對系統(tǒng)內(nèi)存中的某個區(qū)域進行加鎖的原子操作。這些操作常用來管理共享的數(shù)據(jù)結(jié)構(gòu)(如信號量、段描述符、系統(tǒng)段或頁表),兩個或多個處理器可能同時會修改這些數(shù)據(jù)結(jié)構(gòu)中的同一數(shù)據(jù)域或標志。處理器使用三個相互依賴的機制來實現(xiàn)加鎖的原子操作:
    1、保證原子操作
    2、總線加鎖,使用LOCK#信號和LOCK指令前綴
    3、高速緩存相干性協(xié)議,確保對高速緩存中的數(shù)據(jù)結(jié)構(gòu)執(zhí)行原子操作(高速緩存鎖)。這種機制存在于Pentium4、Intel Xeon和P6系列處理器中

    IA-32處理器提供有一個LOCK#信號,會在某些關(guān)鍵內(nèi)存操作期間被自動激活,去鎖定系統(tǒng)總線。當(dāng)這個輸出信號發(fā)出的時候,來自其他處理器或總線代理的控制請求將被阻塞。軟件能夠通過預(yù)先在指令前添加LOCK前綴來指定需要LOCK語義的其它場合。
    在Intel386、Intel486、Pentium處理器中,明確地對指令加鎖會導(dǎo)致LOCK#信號的產(chǎn)生。由硬件設(shè)計人員來保證系統(tǒng)硬件中LOCK#信號的可用性,以控制處理器間的內(nèi)存訪問。
    對于Pentinum4、Intel Xeon以及P6系列處理器,如果被訪問的內(nèi)存區(qū)域是在處理器內(nèi)部進行高速緩存的,那么通常不發(fā)出LOCK#信號;相反,加鎖只應(yīng)用于處理器的高速緩存。

    為顯式地強制執(zhí)行LOCK語義,軟件可以在下列指令修改內(nèi)存區(qū)域時使用LOCK前綴。當(dāng)LOCK前綴被置于其它指令之前或者指令沒有對內(nèi)存進行寫操作(也就是說目標操作數(shù)在寄存器中)時,會產(chǎn)生一個非法操作碼異常(#UD)。
    【1】位測試和修改指令(BTS、BTR、BTC)
    【2】交換指令(XADD、CMPXCHG、CMPXCHG8B)
    【3】自動假設(shè)有LOCK前綴的XCHG指令
    【4】下列單操作數(shù)的算數(shù)和邏輯指令:INC、DEC、NOT、NEG
    【5】下列雙操作數(shù)的算數(shù)和邏輯指令:ADD、ADC、SUB、SBB、AND、OR、XOR
    一個加鎖的指令會保證對目標操作數(shù)所在的內(nèi)存區(qū)域加鎖,但是系統(tǒng)可能會將鎖定區(qū)域解釋得稍大一些。
    軟件應(yīng)該使用相同的地址和操作數(shù)長度來訪問信號量(用作處理器之間發(fā)送信號的共享內(nèi)存)。例如,如果一個處理器使用一個字來訪問信號量,其它處理器就不應(yīng)該使用一個字節(jié)來訪問這個信號量。
    總線鎖的完整性不收內(nèi)存區(qū)域?qū)R的影響。加鎖語義會一直持續(xù),以滿足更新整個操作數(shù)所需的總線周期個數(shù)。但是,建議加鎖訪問應(yīng)該對齊在它們的自然邊界上,以提升系統(tǒng)性能:
    【1】任何8位訪問的邊界(加鎖或不加鎖)
    【2】鎖定的字訪問的16位邊界
    【3】鎖定的雙字訪問的32位邊界
    【4】鎖定的四字訪問的64位邊界
    對所有其它的內(nèi)存操作和所有可見的外部事件來說,加鎖的操作都是原子的。所有取指令和頁表操作能夠越過加鎖的指令。加鎖的指令可用于同步一個處理器寫數(shù)據(jù)而另一個處理器讀數(shù)據(jù)的操作。

    IA-32架構(gòu)提供了幾種機制用來強化或弱化內(nèi)存排序模型,以處理特殊的編程情形。這些機制包括:
    【1】I/O指令、加鎖指令、LOCK前綴以及串行化指令等,強制在處理器上進行較強的排序
    【2】SFENCE指令(在Pentium III中引入)和LFENCE指令、MFENCE指令(在Pentium4和Intel Xeon處理器中引入)提供了某些特殊類型內(nèi)存操作的排序和串行化功能
    …(這里還有兩條就不寫了)
    這些機制可以通過下面的方式使用。
    總線上的內(nèi)存映射設(shè)備和其它I/O設(shè)備通常對向它們緩沖區(qū)寫操作的順序很敏感,I/O指令(IN指令和OUT指令)以下面的方式對這種訪問執(zhí)行強寫操作的排序。在執(zhí)行了一條I/O指令之前,處理器等待之前的所有指令執(zhí)行完畢以及所有的緩沖區(qū)都被都被寫入了內(nèi)存。只有取指令和頁表查詢能夠越過I/O指令,后續(xù)指令要等到I/O指令執(zhí)行完畢才開始執(zhí)行。

    反復(fù)思考IA-32手冊對lock指令作用的這幾段描述,可以得出lock指令的幾個作用:

  • 鎖總線,其它CPU對內(nèi)存的讀寫請求都會被阻塞,直到鎖釋放,不過實際后來的處理器都采用鎖緩存替代鎖總線,因為鎖總線的開銷比較大,鎖總線期間其他CPU沒法訪問內(nèi)存
  • lock后的寫操作會回寫已修改的數(shù)據(jù),同時讓其它CPU相關(guān)緩存行失效,從而重新從主存中加載最新的數(shù)據(jù)
  • 不是內(nèi)存屏障卻能完成類似內(nèi)存屏障的功能,阻止屏障兩遍的指令重排序
  • (1)中寫了由于效率問題,實際后來的處理器都采用鎖緩存來替代鎖總線,這種場景下多緩存的數(shù)據(jù)一致是通過緩存一致性協(xié)議來保證的,我們來看一下什么是緩存一致性協(xié)議。

    緩存一致性協(xié)議

    講緩存一致性之前,先說一下緩存行的概念:

    • 緩存是分段(line)的,一個段對應(yīng)一塊存儲空間,我們稱之為緩存行,它是CPU緩存中可分配的最小存儲單元,大小32字節(jié)、64字節(jié)、128字節(jié)不等,這與CPU架構(gòu)有關(guān),通常來說是64字節(jié)。當(dāng)CPU看到一條讀取內(nèi)存的指令時,它會把內(nèi)存地址傳遞給一級數(shù)據(jù)緩存,一級數(shù)據(jù)緩存會檢查它是否有這個內(nèi)存地址對應(yīng)的緩存段,如果沒有就把整個緩存段從內(nèi)存(或更高一級的緩存)中加載進來。注意,這里說的是一次加載整個緩存段,這就是上面提過的局部性原理

    上面說了,LOCK#會鎖總線,實際上這不現(xiàn)實,因為鎖總線效率太低了。因此最好能做到:使用多組緩存,但是它們的行為看起來只有一組緩存那樣。緩存一致性協(xié)議就是為了做到這一點而設(shè)計的,就像名稱所暗示的那樣,這類協(xié)議就是要使多組緩存的內(nèi)容保持一致。

    緩存一致性協(xié)議有多種,但是日常處理的大多數(shù)計算機設(shè)備都屬于”嗅探(snooping)”協(xié)議,它的基本思想是:

    所有內(nèi)存的傳輸都發(fā)生在一條共享的總線上,而所有的處理器都能看到這條總線:緩存本身是獨立的,但是內(nèi)存是共享資源,所有的內(nèi)存訪問都要經(jīng)過仲裁(同一個指令周期中,只有一個CPU緩存可以讀寫內(nèi)存)。

    CPU緩存不僅僅在做內(nèi)存?zhèn)鬏數(shù)臅r候才與總線打交道,而是不停在嗅探總線上發(fā)生的數(shù)據(jù)交換,跟蹤其他緩存在做什么。所以當(dāng)一個緩存代表它所屬的處理器去讀寫內(nèi)存時,其它處理器都會得到通知,它們以此來使自己的緩存保持同步。只要某個處理器一寫內(nèi)存,其它處理器馬上知道這塊內(nèi)存在它們的緩存段中已失效。
    MESI協(xié)議是當(dāng)前最主流的緩存一致性協(xié)議,在MESI協(xié)議中,每個緩存行有4個狀態(tài),可用2個bit表示,它們分別是:

    這里的I、S和M狀態(tài)已經(jīng)有了對應(yīng)的概念:失效/未載入、干凈以及臟的緩存段。所以這里新的知識點只有E狀態(tài),代表獨占式訪問,這個狀態(tài)解決了”在我們開始修改某塊內(nèi)存之前,我們需要告訴其它處理器”這一問題:只有當(dāng)緩存行處于E或者M狀態(tài)時,處理器才能去寫它,也就是說只有在這兩種狀態(tài)下,處理器是獨占這個緩存行的。當(dāng)處理器想寫某個緩存行時,如果它沒有獨占權(quán),它必須先發(fā)送一條”我要獨占權(quán)”的請求給總線,這會通知其它處理器把它們擁有的同一緩存段的拷貝失效(如果有)。只有在獲得獨占權(quán)后,處理器才能開始修改數(shù)據(jù)—-并且此時這個處理器知道,這個緩存行只有一份拷貝,在我自己的緩存里,所以不會有任何沖突。

    反之,如果有其它處理器想讀取這個緩存行(馬上能知道,因為一直在嗅探總線),獨占或已修改的緩存行必須先回到”共享”狀態(tài)。如果是已修改的緩存行,那么還要先把內(nèi)容回寫到內(nèi)存中。

    由lock指令回看volatile變量讀寫

    相信有了上面對于lock的解釋,volatile關(guān)鍵字的實現(xiàn)原理應(yīng)該是一目了然了。首先看一張圖:

    工作內(nèi)存Work Memory其實就是對CPU寄存器和高速緩存的抽象,或者說每個線程的工作內(nèi)存也可以簡單理解為CPU寄存器和高速緩存。

    那么當(dāng)寫兩條線程Thread-A與Threab-B同時操作主存中的一個volatile變量i時,Thread-A寫了變量i,那么:

    • Thread-A發(fā)出LOCK#指令
    • 發(fā)出的LOCK#指令鎖總線(或鎖緩存行),同時讓Thread-B高速緩存中的緩存行內(nèi)容失效
    • Thread-A向主存回寫最新修改的i

    Thread-B讀取變量i,那么:

    • Thread-B發(fā)現(xiàn)對應(yīng)地址的緩存行被鎖了,等待鎖的釋放,緩存一致性協(xié)議會保證它讀取到最新的值

    由此可以看出,volatile關(guān)鍵字的讀和普通變量的讀取相比基本沒差別,差別主要還是在變量的寫操作上。

    后記

    之前對于volatile關(guān)鍵字的作用我個人還有一些會混淆的誤區(qū),在深入理解volatile關(guān)鍵字的作用之后,感覺對volatile的理解深了許多。相信看到文章這里的你,只要肯想、肯研究,一定會和我一樣有恍然大悟、茅塞頓開的感覺^_^]

    參考資料

    • 《IA-32架構(gòu)軟件開發(fā)人員手冊 第3卷:系統(tǒng)編程指南》
    • 《Java并發(fā)編程的藝術(shù)》
    • 《深入理解Java虛擬機:JVM高級特性與最佳實踐》
    • PrintAssembly查看volatile匯編代碼小記
    • 緩存一致性(Cache Coherency)入門
    • 聊聊高并發(fā)(三十四)Java內(nèi)存模型那些事(二)理解CPU高速緩存的工作原理

    總結(jié)

    以上是生活随笔為你收集整理的就是要你懂 Java 中 volatile 关键字实现原理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。