volatile與lock前綴指令


前言

我們知道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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
  * @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文件,看一下生成的字節碼:

沒有任何特別的。要知道,字節碼指令,比如上圖的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函數之前,加入如下虛擬機參數:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
Java HotSpot(TM)  64 -Bit Server VM warning: PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional output
CompilerOracle: compileonly *LazySingleton.getInstance
Loaded disassembler from D:\JDK\jre\bin\server\hsdis-amd64.dll
Decoding 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兩行:

1
2
0x0000000002931351 : lock add dword ptr [rsp],0h ;*putstatic instance
; - 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指令的作用在手冊中散落在各處,並不是在某一章或者某一節專門講):

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

(從Pentium 4,Intel Xeon及P6處理器開始,intel在原有總線鎖的基礎上做了一個很有意義的優化:如果要訪問的內存區域(area of memory)在lock前綴指令執行期間已經在處理器內部的緩存中被鎖定(即包含該內存區域的緩存行當前處於獨占或以修改狀態),並且該內存區域被完全包含在單個緩存行(cache line)中,那么處理器將直接執行該指令。由於在指令執行期間該緩存行會一直被鎖定,其它處理器無法讀/寫該指令要訪問的內存區域,因此能保證指令執行的原子性。這個操作過程叫做緩存鎖定(cache locking),緩存鎖定將大大降低lock前綴指令的執行開銷,但是當多處理器之間的競爭程度很高或者指令訪問的內存地址未對齊時,仍然會鎖住總線。摘自 這里)ps:從Pentium 4,Intel Xeon及P6處理器開始,包括以后的處理器都有了緩存鎖。

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

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

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

IA-32架構提供了幾種機制用來強化或弱化內存排序模型,以處理特殊的編程情形。這些機制包括:
【1】I/O指令、加鎖指令、LOCK前綴以及串行化指令等,強制在處理器上進行較強的排序
【2】SFENCE指令(在Pentium III中引入)和LFENCE指令、MFENCE指令(在Pentium4和Intel Xeon處理器中引入)提供了某些特殊類型內存操作的排序和串行化功能
…(這里還有兩條就不寫了)
這些機制可以通過下面的方式使用。
總線上的內存映射設備和其它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)”協議,它的基本思想是:

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

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關鍵字的讀和普通變量的讀取相比基本沒差別,差別主要還是在變量的寫操作上。

后記

 

為什么static volatile int i = 0; i++;不保證線程安全?

因為i++並不是一個原子操作,它包含了三步(實際上對應的機器碼步驟更多,但是這里分解為三步已經足夠說明問題):

1、獲取i
2、i自增
3、回寫i


A、B兩個線程同時自增i
由於volatile可見性,因此步驟1兩條線程一定拿到的是最新的i,也就是相同的i
但是從第2步開始就有問題了,有可能出現的場景是線程A自增了i並回寫,但是線程B此時已經拿到了i,不會再去拿線程A回寫的i,因此對原值進行了一次自增並回寫
這就導致了線程非安全,也就是你說的多線程技術器結果不對

總而言之,volatile只能保證拿到的變量一定最新的,至於拿到的變量做了其他操作,volatile無法也沒有辦法保證它們的線程安全性

也許你會問,如果線程A對i進行自增了以后cpu緩存不是應該通知其他緩存,並且重新load i么?

拿的前提是讀,問題是,線程A對i進行了自增,線程B已經拿到了i並不存在需要再次讀取i的場景,當然是不會重新load i這個值的。

ps:也就是線程B的緩存行內容的確會失效。但是此時線程B中i的值已經運行在加法指令中,不存在需要再次從緩存行讀取i的場景。

參考資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM