java volatile原理A CUP層面

作者:知乎用戶
鏈接:https://www.zhihu.com/question/49656589/answer/117826278
來源:知乎
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
 

volatile 只能保證 “可見性”,不能保證 “原子性”。

count++; 這條語句由3條指令組成:
(1)將 count 的值從內存加載到 cpu 的某個寄存器r
(2)將 寄存器r 的值 +1,結果存放在 寄存器s
(3)將 寄存器s 中的值寫回內存

所以,如果有多個線程同時在執行 count++;,在某個線程執行完第(3)步之前,其它線程是看不到它的執行結果的。

在沒有 volatile 的時候,執行完 count++;,執行結果其實是寫到CPU緩存中,沒有馬上寫回到內存中,後續在某些情況下(比如CPU緩存不夠用)再將CPU緩存中的值flush到內存。正因爲沒有馬上寫到內存,所以不能保證其它線程可以及時見到執行的結果。
在有 volatile 的時候,執行完 count++;,執行結果寫到CPU緩存中,並且同時寫回到內存,因爲已經寫回內存了,所以可以保證其它線程馬上看到執行的結果。
但是,volatile 並沒有保證原子性,在某個線程執行(1)(2)(3)的時候,volatile 並沒有鎖定 count 的值,也就是並不能阻塞其他線程也執行(1)(2)(3)。可能有兩個線程同時執行(1),所以(2)計算出來一樣的結果,然後(3)存回的也是同一個值。

補充幾篇資料:
(1)java 內存模型:Java Memory Model
(2)java volatile 關鍵字:Java Volatile Keyword
(3)使用 volatile 的一些pattern:Java theory and practice: Managing volatility

 

前言

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

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

 

CPU緩存

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

  • 一次主內存的訪問通常在幾十到幾百個時鐘週期
  • 一次L1高速緩存的讀寫只需要1~2個時鐘週期
  • 一次L2高速緩存的讀寫也只需要數十個時鐘週期

這種訪問速度的顯著差異,導致CPU可能會花費很長時間等待數據到來或把數據寫入內存。

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

按照讀取順序與CPU結合的緊密程度,CPU緩存可分爲:

  • 一級緩存:簡稱L1 Cache,位於CPU內核的旁邊,是與CPU結合最爲緊密的CPU緩存
  • 二級緩存:簡稱L2 Cache,分內部和外部兩種芯片,內部芯片二級緩存運行速度與主頻相同,外部芯片二級緩存運行速度則只有主頻的一半
  • 三級緩存:簡稱L3 Cache,部分高端CPU纔有

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

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

 

使用CPU緩存帶來的問題

用一張圖表示一下CPU-->CPU緩存-->主內存數據讀取之間的關係:

當系統運行時,CPU執行計算的過程如下:

  1. 程序以及數據被加載到主內存
  2. 指令和數據被加載到CPU緩存
  3. CPU執行指令,把結果寫到高速緩存
  4. 高速緩存中的數據寫回主內存

如果服務器是單核CPU,那麼這些步驟不會有任何的問題,但是如果服務器是多核CPU,那麼問題來了,以Intel Core i7處理器的高速緩存概念模型爲例(圖片摘自《深入理解計算機系統》):

試想下面一種情況:

  1. 核0讀取了一個字節,根據局部性原理,它相鄰的字節同樣被被讀入核0的緩存
  2. 核3做了上面同樣的工作,這樣核0與核3的緩存擁有同樣的數據
  3. 核0修改了那個字節,被修改後,那個字節被寫回核0的緩存,但是該信息並沒有寫回主存
  4. 核3訪問該字節,由於核0並未將數據寫回主存,數據不同步

爲了解決這個問題,CPU製造商制定了一個規則:當一個CPU修改緩存中的字節時,服務器中其他CPU會被通知,它們的緩存將視爲無效。於是,在上面的情況下,核3發現自己的緩存中數據已無效,核0將立即把自己的數據寫回主存,然後核3重新讀取該數據。

 

反彙編Java字節碼,查看彙編層面對volatile關鍵字做了什麼

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

複製代碼


 
  1. 1 /**

  2. 2 * @author 五月的倉頡http://www.cnblogs.com/xrq730/p/7048693.html

  3. 3 */

  4. 4 public class LazySingleton {

  5. 5

  6. 6 private static volatile LazySingleton instance = null;

  7. 7

  8. 8 public static LazySingleton getInstance() {

  9. 9 if (instance == null) {

  10. 10 instance = new LazySingleton();

  11. 11 }

  12. 12

  13. 13 return instance;

  14. 14 }

  15. 15

  16. 16 public static void main(String[] args) {

  17. 17 LazySingleton.getInstance();

  18. 18 }

  19. 19

  20. 20 }

複製代碼

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

沒有任何特別的。要知道,字節碼指令,比如上圖的getstatic、ifnonnull、new等,最終對應到操作系統的層面,都是轉換爲一條一條指令去執行,我們使用的PC機、應用服務器的CPU架構通常都是IA-32架構的,這種架構採用的指令集是CISC(複雜指令集),而彙編語言則是這種指令集的助記符。

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

然後跑main函數,跑main函數之前,加入如下虛擬機參數:

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

運行main函數即可,代碼生成的彙編指令爲:

複製代碼


 
  1. 1 Java HotSpot(TM) 64-Bit Server VM warning: PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional output

  2. 2 CompilerOracle: compileonly *LazySingleton.getInstance

  3. 3 Loaded disassembler from D:\JDK\jre\bin\server\hsdis-amd64.dll

  4. 4 Decoding compiled method 0x0000000002931150:

  5. 5 Code:

  6. 6 Argument 0 is unknown.RIP: 0x29312a0 Code size: 0x00000108

  7. 7 [Disassembling for mach='amd64']

  8. 8 [Entry Point]

  9. 9 [Verified Entry Point]

  10. 10 [Constants]

  11. 11 # {method} 'getInstance' '()Lorg/xrq/test/design/singleton/LazySingleton;' in 'org/xrq/test/design/singleton/LazySingleton'

  12. 12 # [sp+0x20] (sp of caller)

  13. 13 0x00000000029312a0: mov dword ptr [rsp+0ffffffffffffa000h],eax

  14. 14 0x00000000029312a7: push rbp

  15. 15 0x00000000029312a8: sub rsp,10h ;*synchronization entry

  16. 16 ; - org.xrq.test.design.singleton.LazySingleton::getInstance@-1 (line 13)

  17. 17 0x00000000029312ac: mov r10,7ada9e428h ; {oop(a 'java/lang/Class' = 'org/xrq/test/design/singleton/LazySingleton')}

  18. 18 0x00000000029312b6: mov r11d,dword ptr [r10+58h]

  19. 19 ;*getstatic instance

  20. 20 ; - org.xrq.test.design.singleton.LazySingleton::getInstance@0 (line 13)

  21. 21 0x00000000029312ba: test r11d,r11d

  22. 22 0x00000000029312bd: je 29312e0h

  23. 23 0x00000000029312bf: mov r10,7ada9e428h ; {oop(a 'java/lang/Class' = 'org/xrq/test/design/singleton/LazySingleton')}

  24. 24 0x00000000029312c9: mov r11d,dword ptr [r10+58h]

  25. 25 0x00000000029312cd: mov rax,r11

  26. 26 0x00000000029312d0: shl rax,3h ;*getstatic instance

  27. 27 ; - org.xrq.test.design.singleton.LazySingleton::getInstance@16 (line 17)

  28. 28 0x00000000029312d4: add rsp,10h

  29. 29 0x00000000029312d8: pop rbp

  30. 30 0x00000000029312d9: test dword ptr [330000h],eax ; {poll_return}

  31. 31 0x00000000029312df: ret

  32. 32 0x00000000029312e0: mov rax,qword ptr [r15+60h]

  33. 33 0x00000000029312e4: mov r10,rax

  34. 34 0x00000000029312e7: add r10,10h

  35. 35 0x00000000029312eb: cmp r10,qword ptr [r15+70h]

  36. 36 0x00000000029312ef: jnb 293135bh

  37. 37 0x00000000029312f1: mov qword ptr [r15+60h],r10

  38. 38 0x00000000029312f5: prefetchnta byte ptr [r10+0c0h]

  39. 39 0x00000000029312fd: mov r11d,0e07d00b2h ; {oop('org/xrq/test/design/singleton/LazySingleton')}

  40. 40 0x0000000002931303: mov r10,qword ptr [r12+r11*8+0b0h]

  41. 41 0x000000000293130b: mov qword ptr [rax],r10

  42. 42 0x000000000293130e: mov dword ptr [rax+8h],0e07d00b2h

  43. 43 ; {oop('org/xrq/test/design/singleton/LazySingleton')}

  44. 44 0x0000000002931315: mov dword ptr [rax+0ch],r12d

  45. 45 0x0000000002931319: mov rbp,rax ;*new ; - org.xrq.test.design.singleton.LazySingleton::getInstance@6 (line 14)

  46. 46 0x000000000293131c: mov rdx,rbp

  47. 47 0x000000000293131f: call 2907c60h ; OopMap{rbp=Oop off=132}

  48. 48 ;*invokespecial <init>

  49. 49 ; - org.xrq.test.design.singleton.LazySingleton::getInstance@10 (line 14)

  50. 50 ; {optimized virtual_call}

  51. 51 0x0000000002931324: mov r10,rbp

  52. 52 0x0000000002931327: shr r10,3h

  53. 53 0x000000000293132b: mov r11,7ada9e428h ; {oop(a 'java/lang/Class' = 'org/xrq/test/design/singleton/LazySingleton')}

  54. 54 0x0000000002931335: mov dword ptr [r11+58h],r10d

  55. 55 0x0000000002931339: mov r10,7ada9e428h ; {oop(a 'java/lang/Class' = 'org/xrq/test/design/singleton/LazySingleton')}

  56. 56 0x0000000002931343: shr r10,9h

  57. 57 0x0000000002931347: mov r11d,20b2000h

  58. 58 0x000000000293134d: mov byte ptr [r11+r10],r12l

  59. 59 0x0000000002931351: lock add dword ptr [rsp],0h ;*putstatic instance

  60. 60 ; - org.xrq.test.design.singleton.LazySingleton::getInstance@13 (line 14)

  61. 61 0x0000000002931356: jmp 29312bfh

  62. 62 0x000000000293135b: mov rdx,703e80590h ; {oop('org/xrq/test/design/singleton/LazySingleton')}

  63. 63 0x0000000002931365: nop

  64. 64 0x0000000002931367: call 292fbe0h ; OopMap{off=204}

  65. 65 ;*new ; - org.xrq.test.design.singleton.LazySingleton::getInstance@6 (line 14)

  66. 66 ; {runtime_call}

  67. 67 0x000000000293136c: jmp 2931319h

  68. 68 0x000000000293136e: mov rdx,rax

  69. 69 0x0000000002931371: jmp 2931376h

  70. 70 0x0000000002931373: mov rdx,rax ;*new ; - org.xrq.test.design.singleton.LazySingleton::getInstance@6 (line 14)

  71. 71 0x0000000002931376: add rsp,10h

  72. 72 0x000000000293137a: pop rbp

  73. 73 0x000000000293137b: jmp 2932b20h ; {runtime_call}

  74. 74 [Stub Code]

  75. 75 0x0000000002931380: mov rbx,0h ; {no_reloc}

  76. 76 0x000000000293138a: jmp 293138ah ; {runtime_call}

  77. 77 [Exception Handler]

  78. 78 0x000000000293138f: jmp 292fca0h ; {runtime_call}

  79. 79 [Deopt Handler Code]

  80. 80 0x0000000002931394: call 2931399h

  81. 81 0x0000000002931399: sub qword ptr [rsp],5h

  82. 82 0x000000000293139e: jmp 2909000h ; {runtime_call}

  83. 83 0x00000000029313a3: hlt

  84. 84 0x00000000029313a4: hlt

  85. 85 0x00000000029313a5: hlt

  86. 86 0x00000000029313a6: hlt

  87. 87 0x00000000029313a7: hlt

複製代碼

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


 
  1. 0x0000000002931351: lock add dword ptr [rsp],0h ;*putstatic instance

  2. ; - org.xrq.test.design.singleton.LazySingleton::getInstance@13 (line 14)

之所以定位到這兩行是因爲這裏結尾寫明瞭line 14,line 14即volatile變量instance賦值的地方。後面的add dword ptr [rsp],0h都是正常的彙編語句,意思是將雙字節的棧指針寄存器+0,這裏的關鍵就是add前面的lock指令,後面詳細分析一下lock指令的作用和爲什麼加上lock指令後就能保證volatile關鍵字的內存可見性。

 

lock指令做了什麼

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

我摘抄一下IA-32手冊中關於lock指令作用的一些描述(因爲lock指令的作用在手冊中散落在各處,並不是在某一章或者某一節專門講): 


 
  1. 在修改內存操作時,使用LOCK前綴去調用加鎖的讀-修改-寫操作,這種機制用於多處理器系統中處理器之間進行可靠的通訊,具體描述如下:

  2. (1)在Pentium和早期的IA-32處理器中,LOCK前綴會使處理器執行當前指令時產生一個LOCK#信號,這種總是引起顯式總線鎖定出現

  3. (2)在Pentium4、Inter Xeon和P6系列處理器中,加鎖操作是由高速緩存鎖或總線鎖來處理。如果內存訪問有高速緩存且隻影響一個單獨的高速緩存行,那麼操作中就會調用高速緩存鎖,而系統總線和系統內存中的實際區域內不會被鎖定。同時,這條總線上的其它Pentium4、Intel Xeon或者P6系列處理器就回寫所有已修改的數據並使它們的高速緩存失效,以保證系統內存的一致性。如果內存訪問沒有高速緩存且/或它跨越了高速緩存行的邊界,那麼這個處理器就會產生LOCK#信號,並在鎖定操作期間不會響應總線控制請求


 
  1. 32位IA-32處理器支持對系統內存中的某個區域進行加鎖的原子操作。這些操作常用來管理共享的數據結構(如信號量、段描述符、系統段或頁表),兩個或多個處理器可能同時會修改這些數據結構中的同一數據域或標誌。處理器使用三個相互依賴的機制來實現加鎖的原子操作:

  2. 1、保證原子操作

  3. 2、總線加鎖,使用LOCK#信號和LOCK指令前綴

  4. 3、高速緩存相干性協議,確保對高速緩存中的數據結構執行原子操作(高速緩存鎖)。這種機制存在於Pentium4、Intel Xeon和P6系列處理器中


 
  1. IA-32處理器提供有一個LOCK#信號,會在某些關鍵內存操作期間被自動激活,去鎖定系統總線。當這個輸出信號發出的時候,來自其他處理器或總線代理的控制請求將被阻塞。軟件能夠通過預先在指令前添加LOCK前綴來指定需要LOCK語義的其它場合。

  2. 在Intel386、Intel486、Pentium處理器中,明確地對指令加鎖會導致LOCK#信號的產生。由硬件設計人員來保證系統硬件中LOCK#信號的可用性,以控制處理器間的內存訪問。

  3. 對於Pentinum4、Intel Xeon以及P6系列處理器,如果被訪問的內存區域是在處理器內部進行高速緩存的,那麼通常不發出LOCK#信號;相反,加鎖只應用於處理器的高速緩存。

複製代碼


 
  1. 爲顯式地強制執行LOCK語義,軟件可以在下列指令修改內存區域時使用LOCK前綴。當LOCK前綴被置於其它指令之前或者指令沒有對內存進行寫操作(也就是說目標操作數在寄存器中)時,會產生一個非法操作碼異常(#UD)。

  2. 【1】位測試和修改指令(BTS、BTR、BTC)

  3. 【2】交換指令(XADD、CMPXCHG、CMPXCHG8B)

  4. 【3】自動假設有LOCK前綴的XCHG指令

  5. 【4】下列單操作數的算數和邏輯指令:INC、DEC、NOT、NEG

  6. 【5】下列雙操作數的算數和邏輯指令:ADD、ADC、SUB、SBB、AND、OR、XOR

  7. 一個加鎖的指令會保證對目標操作數所在的內存區域加鎖,但是系統可能會將鎖定區域解釋得稍大一些。

  8. 軟件應該使用相同的地址和操作數長度來訪問信號量(用作處理器之間發送信號的共享內存)。例如,如果一個處理器使用一個字來訪問信號量,其它處理器就不應該使用一個字節來訪問這個信號量。

  9. 總線鎖的完整性不收內存區域對齊的影響。加鎖語義會一直持續,以滿足更新整個操作數所需的總線週期個數。但是,建議加鎖訪問應該對齊在它們的自然邊界上,以提升系統性能:

  10. 【1】任何8位訪問的邊界(加鎖或不加鎖)

  11. 【2】鎖定的字訪問的16位邊界

  12. 【3】鎖定的雙字訪問的32位邊界

  13. 【4】鎖定的四字訪問的64位邊界

  14. 對所有其它的內存操作和所有可見的外部事件來說,加鎖的操作都是原子的。所有取指令和頁表操作能夠越過加鎖的指令。加鎖的指令可用於同步一個處理器寫數據而另一個處理器讀數據的操作。

複製代碼

複製代碼


 
  1. IA-32架構提供了幾種機制用來強化或弱化內存排序模型,以處理特殊的編程情形。這些機制包括:

  2. 【1】I/O指令、加鎖指令、LOCK前綴以及串行化指令等,強制在處理器上進行較強的排序

  3. 【2】SFENCE指令(在Pentium III中引入)和LFENCE指令、MFENCE指令(在Pentium4和Intel Xeon處理器中引入)提供了某些特殊類型內存操作的排序和串行化功能

  4. ...(這裏還有兩條就不寫了)

  5. 這些機制可以通過下面的方式使用。

  6. 總線上的內存映射設備和其它I/O設備通常對向它們緩衝區寫操作的順序很敏感,I/O指令(IN指令和OUT指令)以下面的方式對這種訪問執行強寫操作的排序。在執行了一條I/O指令之前,處理器等待之前的所有指令執行完畢以及所有的緩衝區都被都被寫入了內存。只有取指令和頁表查詢能夠越過I/O指令,後續指令要等到I/O指令執行完畢纔開始執行。

複製代碼

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

  1. 鎖總線,其它CPU對內存的讀寫請求都會被阻塞,直到鎖釋放,不過實際後來的處理器都採用鎖緩存替代鎖總線,因爲鎖總線的開銷比較大,鎖總線期間其他CPU沒法訪問內存
  2. lock後的寫操作會回寫已修改的數據,同時讓其它CPU相關緩存行失效,從而重新從主存中加載最新的數據
  3. 不是內存屏障卻能完成類似內存屏障的功能,阻止屏障兩遍的指令重排序

(1)中寫了由於效率問題,實際後來的處理器都採用鎖緩存來替代鎖總線,這種場景下多緩存的數據一致是通過緩存一致性協議來保證的,我們來看一下什麼是緩存一致性協議。 

 

緩存一致性協議

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

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

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

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


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

  2.  
  3. CPU緩存不僅僅在做內存傳輸的時候才與總線打交道,而是不停在嗅探總線上發生的數據交換,跟蹤其他緩存在做什麼。所以當一個緩存代表它所屬的處理器去讀寫內存時,其它處理器都會得到通知,它們以此來使自己的緩存保持同步。只要某個處理器一寫內存,其它處理器馬上知道這塊內存在它們的緩存段中已失效。

MESI協議是當前最主流的緩存一致性協議,在MESI協議中,每個緩存行有4個狀態,可用2個bit表示,它們分別是:

這裏的I、S和M狀態已經有了對應的概念:失效/未載入、乾淨以及髒的緩存段。所以這裏新的知識點只有E狀態,代表獨佔式訪問,這個狀態解決了"在我們開始修改某塊內存之前,我們需要告訴其它處理器"這一問題:只有當緩存行處於E或者M狀態時,處理器才能去寫它,也就是說只有在這兩種狀態下,處理器是獨佔這個緩存行的。當處理器想寫某個緩存行時,如果它沒有獨佔權,它必須先發送一條"我要獨佔權"的請求給總線,這會通知其它處理器把它們擁有的同一緩存段的拷貝失效(如果有)。只有在獲得獨佔權後,處理器才能開始修改數據----並且此時這個處理器知道,這個緩存行只有一份拷貝,在我自己的緩存裏,所以不會有任何衝突。

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

 

由lock指令回看volatile變量讀寫

相信有了上面對於lock的解釋,volatile關鍵字的實現原理應該是一目瞭然了。首先看一張圖:

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

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

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

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

  • Thread-B發現對應地址的緩存行被鎖了,等待鎖的釋放,緩存一致性協議會保證它讀取到最新的值

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

 

volatile 和 synchronized 的區別?

  1. volatile 本質是在告訴 JVM 當前變量在寄存器(工作內存)中的值是不確定的,需要從主存中讀取。synchronized 則是鎖定當前變量,只有當前線程可以訪問該變量,其他線程被阻塞住。
  2. volatile 僅能使用在變量級別。synchronized 則可以使用在變量、方法、和類級別的。
  3. volatile 僅能實現變量的修改可見性,不能保證原子性。而synchronized 則可以保證變量的修改可見性和原子性。
  4. volatile 不會造成線程的阻塞。synchronized 可能會造成線程的阻塞。
  5. volatile 標記的變量不會被編譯器優化。synchronized標記的變量可以被編譯器優化。

另外,會有面試官會問 volatile 能否取代 synchronized 呢?答案肯定是不能,雖然說 volatile 被稱之爲輕量級鎖,但是和 synchronized 是有本質上的區別,原因就是上面的幾點落。

什麼場景下可以使用 volatile 替換 synchronized ?

  1. 只需要保證共享資源的可見性的時候可以使用 volatile 替代,synchronized 保證可操作的原子性一致性和可見性。
  2. volatile 適用於新值不依賴於舊值的情形。
  3. 1 寫 N 讀。
  4. 不與其他變量構成不變性條件時候使用 volatile 。

後記

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章