一区二区三区在线-一区二区三区亚洲视频-一区二区三区亚洲-一区二区三区午夜-一区二区三区四区在线视频-一区二区三区四区在线免费观看

服務(wù)器之家:專注于服務(wù)器技術(shù)及軟件下載分享
分類導(dǎo)航

PHP教程|ASP.NET教程|Java教程|ASP教程|編程技術(shù)|正則表達(dá)式|C/C++|IOS|C#|Swift|Android|VB|R語言|JavaScript|易語言|vb.net|

服務(wù)器之家 - 編程語言 - Java教程 - Java中volatile關(guān)鍵字實(shí)現(xiàn)原理

Java中volatile關(guān)鍵字實(shí)現(xiàn)原理

2020-11-22 22:36五月的倉頡 Java教程

本文詳細(xì)解讀一下volatile關(guān)鍵字如何保證變量在多線程之間的可見性,對(duì)Java中volatile關(guān)鍵字實(shí)現(xiàn)原理感興趣的朋友一起通過本文學(xué)習(xí)吧

前言

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

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

cpu緩存

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

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

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

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

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

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

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

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

使用cpu緩存帶來的問題

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

Java中volatile關(guān)鍵字實(shí)現(xiàn)原理

當(dāng)系統(tǒng)運(yùn)行時(shí),cpu執(zhí)行計(jì)算的過程如下:

  1. 程序以及數(shù)據(jù)被加載到主內(nèi)存
  2. 指令和數(shù)據(jù)被加載到cpu緩存
  3. cpu執(zhí)行指令,把結(jié)果寫到高速緩存
  4. 高速緩存中的數(shù)據(jù)寫回主內(nèi)存

如果服務(wù)器是單核cpu,那么這些步驟不會(huì)有任何的問題,但是如果服務(wù)器是多核cpu,那么問題來了,以intel core i7處理器的高速緩存概念模型為例(圖片摘自《深入理解計(jì)算機(jī)系統(tǒng)》):

Java中volatile關(guān)鍵字實(shí)現(xiàn)原理

試想下面一種情況:

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

為了解決這個(gè)問題,cpu制造商制定了一個(gè)規(guī)則:當(dāng)一個(gè)cpu修改緩存中的字節(jié)時(shí),服務(wù)器中其他cpu會(huì)被通知,它們的緩存將視為無效。于是,在上面的情況下,核3發(fā)現(xiàn)自己的緩存中數(shù)據(jù)已無效,核0將立即把自己的數(shù)據(jù)寫回主存,然后核3重新讀取該數(shù)據(jù)。

可以看出,緩存在多核cpu的使用情況下會(huì)有一些性能的缺失。

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

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** * @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é)碼:

Java中volatile關(guān)鍵字實(shí)現(xiàn)原理

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

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

Java中volatile關(guān)鍵字實(shí)現(xiàn)原理

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

?
1
-server -xcomp -xx:+unlockdiagnosticvmoptions -xx:+printassembly -xx:compilecommand=compileonly,*lazysingleton.getinstance

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

?
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)

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

lock指令做了什么

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

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

在修改內(nèi)存操作時(shí),使用lock前綴去調(diào)用加鎖的讀-修改-寫操作,這種機(jī)制用于多處理器系統(tǒng)中處理器之間進(jìn)行可靠的通訊,具體描述如下:

(1)在pentium和早期的ia-32處理器中,lock前綴會(huì)使處理器執(zhí)行當(dāng)前指令時(shí)產(chǎn)生一個(gè)lock#信號(hào),這種總是引起顯式總線鎖定出現(xiàn)

(2)在pentium4、inter xeon和p6系列處理器中,加鎖操作是由高速緩存鎖或總線鎖來處理。如果內(nèi)存訪問有高速緩存且只影響一個(gè)單獨(dú)的高速緩存行,那么操作中就會(huì)調(diào)用高速緩存鎖,而系統(tǒng)總線和系統(tǒng)內(nèi)存中的實(shí)際區(qū)域內(nèi)不會(huì)被鎖定。同時(shí),這條總線上的其它pentium4、intel xeon或者p6系列處理器就回寫所有已修改的數(shù)據(jù)并使它們的高速緩存失效,以保證系統(tǒng)內(nèi)存的一致性。如果內(nèi)存訪問沒有高速緩存且/或它跨越了高速緩存行的邊界,那么這個(gè)處理器就會(huì)產(chǎn)生lock#信號(hào),并在鎖定操作期間不會(huì)響應(yīng)總線控制請求
32位ia-32處理器支持對(duì)系統(tǒng)內(nèi)存中的某個(gè)區(qū)域進(jìn)行加鎖的原子操作。這些操作常用來管理共享的數(shù)據(jù)結(jié)構(gòu)(如信號(hào)量、段描述符、系統(tǒng)段或頁表),兩個(gè)或多個(gè)處理器可能同時(shí)會(huì)修改這些數(shù)據(jù)結(jié)構(gòu)中的同一數(shù)據(jù)域或標(biāo)志。處理器使用三個(gè)相互依賴的機(jī)制來實(shí)現(xiàn)加鎖的原子操作:

1、保證原子操作

2、總線加鎖,使用lock#信號(hào)和lock指令前綴

3、高速緩存相干性協(xié)議,確保對(duì)高速緩存中的數(shù)據(jù)結(jié)構(gòu)執(zhí)行原子操作(高速緩存鎖)。這種機(jī)制存在于pentium4、intel xeon和p6系列處理器中

ia-32處理器提供有一個(gè)lock#信號(hào),會(huì)在某些關(guān)鍵內(nèi)存操作期間被自動(dòng)激活,去鎖定系統(tǒng)總線。當(dāng)這個(gè)輸出信號(hào)發(fā)出的時(shí)候,來自其他處理器或總線代理的控制請求將被阻塞。軟件能夠通過預(yù)先在指令前添加lock前綴來指定需要lock語義的其它場合。

在intel386、intel486、pentium處理器中,明確地對(duì)指令加鎖會(huì)導(dǎo)致lock#信號(hào)的產(chǎn)生。由硬件設(shè)計(jì)人員來保證系統(tǒng)硬件中l(wèi)ock#信號(hào)的可用性,以控制處理器間的內(nèi)存訪問。

對(duì)于pentinum4、intel xeon以及p6系列處理器,如果被訪問的內(nèi)存區(qū)域是在處理器內(nèi)部進(jìn)行高速緩存的,那么通常不發(fā)出lock#信號(hào);相反,加鎖只應(yīng)用于處理器的高速緩存。

為顯式地強(qiáng)制執(zhí)行l(wèi)ock語義,軟件可以在下列指令修改內(nèi)存區(qū)域時(shí)使用lock前綴。當(dāng)lock前綴被置于其它指令之前或者指令沒有對(duì)內(nèi)存進(jìn)行寫操作(也就是說目標(biāo)操作數(shù)在寄存器中)時(shí),會(huì)產(chǎn)生一個(gè)非法操作碼異常(#ud)。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
1】位測試和修改指令(bts、btr、btc)
2】交換指令(xadd、cmpxchg、cmpxchg8b)
3】自動(dòng)假設(shè)有l(wèi)ock前綴的xchg指令
4】下列單操作數(shù)的算數(shù)和邏輯指令:inc、dec、not、neg
5】下列雙操作數(shù)的算數(shù)和邏輯指令:add、adc、sub、sbb、and、or、xor
一個(gè)加鎖的指令會(huì)保證對(duì)目標(biāo)操作數(shù)所在的內(nèi)存區(qū)域加鎖,但是系統(tǒng)可能會(huì)將鎖定區(qū)域解釋得稍大一些。
軟件應(yīng)該使用相同的地址和操作數(shù)長度來訪問信號(hào)量(用作處理器之間發(fā)送信號(hào)的共享內(nèi)存)。例如,如果一個(gè)處理器使用一個(gè)字來訪問信號(hào)量,其它處理器就不應(yīng)該使用一個(gè)字節(jié)來訪問這個(gè)信號(hào)量。
總線鎖的完整性不收內(nèi)存區(qū)域?qū)R的影響。加鎖語義會(huì)一直持續(xù),以滿足更新整個(gè)操作數(shù)所需的總線周期個(gè)數(shù)。但是,建議加鎖訪問應(yīng)該對(duì)齊在它們的自然邊界上,以提升系統(tǒng)性能:
1】任何8位訪問的邊界(加鎖或不加鎖)
2】鎖定的字訪問的16位邊界
3】鎖定的雙字訪問的32位邊界
4】鎖定的四字訪問的64位邊界
對(duì)所有其它的內(nèi)存操作和所有可見的外部事件來說,加鎖的操作都是原子的。所有取指令和頁表操作能夠越過加鎖的指令。加鎖的指令可用于同步一個(gè)處理器寫數(shù)據(jù)而另一個(gè)處理器讀數(shù)據(jù)的操作。

ia-32架構(gòu)提供了幾種機(jī)制用來強(qiáng)化或弱化內(nèi)存排序模型,以處理特殊的編程情形。這些機(jī)制包括:

【1】i/o指令、加鎖指令、lock前綴以及串行化指令等,強(qiáng)制在處理器上進(jìn)行較強(qiáng)的排序

【2】sfence指令(在pentium iii中引入)和lfence指令、mfence指令(在pentium4和intel xeon處理器中引入)提供了某些特殊類型內(nèi)存操作的排序和串行化功能
...(這里還有兩條就不寫了)

這些機(jī)制可以通過下面的方式使用。

總線上的內(nèi)存映射設(shè)備和其它i/o設(shè)備通常對(duì)向它們緩沖區(qū)寫操作的順序很敏感,i/o指令(in指令和out指令)以下面的方式對(duì)這種訪問執(zhí)行強(qiáng)寫操作的排序。在執(zhí)行了一條i/o指令之前,處理器等待之前的所有指令執(zhí)行完畢以及所有的緩沖區(qū)都被都被寫入了內(nèi)存。只有取指令和頁表查詢能夠越過i/o指令,后續(xù)指令要等到i/o指令執(zhí)行完畢才開始執(zhí)行。

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

  1. 鎖總線,其它c(diǎn)pu對(duì)內(nèi)存的讀寫請求都會(huì)被阻塞,直到鎖釋放,不過實(shí)際后來的處理器都采用鎖緩存替代鎖總線,因?yàn)殒i總線的開銷比較大,鎖總線期間其他cpu沒法訪問內(nèi)存
  2. lock后的寫操作會(huì)回寫已修改的數(shù)據(jù),同時(shí)讓其它c(diǎn)pu相關(guān)緩存行失效,從而重新從主存中加載最新的數(shù)據(jù)
  3. 不是內(nèi)存屏障卻能完成類似內(nèi)存屏障的功能,阻止屏障兩遍的指令重排序

(1)中寫了由于效率問題,實(shí)際后來的處理器都采用鎖緩存來替代鎖總線,這種場景下多緩存的數(shù)據(jù)一致是通過緩存一致性協(xié)議來保證的,我們來看一下什么是緩存一致性協(xié)議。

緩存一致性協(xié)議

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

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

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

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

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

cpu緩存不僅僅在做內(nèi)存?zhèn)鬏數(shù)臅r(shí)候才與總線打交道,而是不停在嗅探總線上發(fā)生的數(shù)據(jù)交換,跟蹤其他緩存在做什么。所以當(dāng)一個(gè)緩存代表它所屬的處理器去讀寫內(nèi)存時(shí),其它處理器都會(huì)得到通知,它們以此來使自己的緩存保持同步。只要某個(gè)處理器一寫內(nèi)存,其它處理器馬上知道這塊內(nèi)存在它們的緩存段中已失效。

mesi協(xié)議是當(dāng)前最主流的緩存一致性協(xié)議,在mesi協(xié)議中,每個(gè)緩存行有4個(gè)狀態(tài),可用2個(gè)bit表示,它們分別是:

Java中volatile關(guān)鍵字實(shí)現(xiàn)原理

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

反之,如果有其它處理器想讀取這個(gè)緩存行(馬上能知道,因?yàn)橐恢痹谛崽娇偩€),獨(dú)占或已修改的緩存行必須先回到"共享"狀態(tài)。如果是已修改的緩存行,那么還要先把內(nèi)容回寫到內(nèi)存中。

由lock指令回看volatile變量讀寫

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

Java中volatile關(guān)鍵字實(shí)現(xiàn)原理

工作內(nèi)存work memory其實(shí)就是對(duì)cpu寄存器和高速緩存的抽象,或者說每個(gè)線程的工作內(nèi)存也可以簡單理解為cpu寄存器和高速緩存。

那么當(dāng)寫兩條線程thread-a與threab-b同時(shí)操作主存中的一個(gè)volatile變量i時(shí),thread-a寫了變量i,那么:

  • thread-a發(fā)出lock#指令
  • 發(fā)出的lock#指令鎖總線(或鎖緩存行),同時(shí)讓thread-b高速緩存中的緩存行內(nèi)容失效
  • thread-a向主存回寫最新修改的i
  • thread-b讀取變量i,那么:
  • thread-b發(fā)現(xiàn)對(duì)應(yīng)地址的緩存行被鎖了,等待鎖的釋放,緩存一致性協(xié)議會(huì)保證它讀取到最新的值

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

原文鏈接:http://www.cnblogs.com/xrq730/p/7048693.html

延伸 · 閱讀

精彩推薦
主站蜘蛛池模板: 日产欧产va1 | 精品无码人妻一区二区免费AV | 亚洲视频在线一区二区三区 | 亚洲精品黄色 | 精品国语国产在线对白 | 亚洲国产成人久久综合一区 | 日本高清二三四本2021 | 嫩草影院永久在线播放 | 三极黄色 | 亚洲国产成人精品无码区5566 | 91美女在线视频 | 精品一区二区三区免费站 | 三级网站午夜三级 | 第一福利在线观看永久视频 | 高跟丝袜人妖sissy露出调教 | 国产一区二区三区在线观看视频 | 好妈妈7在线观看高清 | 久青草国产在视频在线观看 | 美女用手扒开粉嫩的屁股 | 天天综合色天天综合色sb | 视频在线观看一区二区 | 丰满艳妇亲伦视频 | 国产午夜精品福利久久 | 欧美黑人换爱交换乱理伦片 | 国产极品麻豆91在线 | 国产美女在线一区二区三区 | 欧美视频在线一区 | 亚欧日韩 | 久久久久激情免费观看 | 日本xxx在线观看免费播放 | 日韩成人精品 | 国产va欧美va在线观看 | 韩国三级在线观看 完整版 韩国三级视频网站 | 国产在线观看精品 | 婷婷色伊人| 欧美一区欧美二区 | 亚洲精品视 | 欧美男同猛男 videos 同性 | 秋霞宅宅236理论片 秋霞一级黄色片 | 无限观看社区在线视频 | 国产精品视频久久久 |