跟我volatile從表面到底層

原文來自個人博客月泉的博客html

食用該篇文章,做者建議你最好已經提早了解過:JMM以及CPU緩存一致性協議還有相關的內存屏障的知識而且可以理解CPU的亂序執行,若是做者理解不當,歡迎指出。 若是本文對你有所幫助不妨給 博客的Github點個小星星,提問和文章有什麼地方理解錯了也能夠直接在Github上提交issue

該篇文章討論的議題:java

  • java語義上的volatile
  • 內存屏障
  • JVM的實現
  • 生成的彙編指令
  • 如何保障的的可見性和有序性
  • 爲何volatile不能保證複合操做的原子性

java語義上的volatile

咱們從一個很常見的案例開始出發linux

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Demo demo = new Demo();
        demo.setName("demo-thread");
        demo.start();
        Thread.sleep(1000);
        demo.flag = false;
        demo.join();
        System.out.println(demo.getName() + "線程執行完畢:" + demo.count);
    }

    static class Demo extends Thread{
        boolean flag = true;
        int count = 0;
        @Override
        public void run(){
            while (flag){
                count ++;
            }
        }
    }
}

這段代碼在執行時,會發生沒法中止下來的現象,顯而易見,demo-thread從主內存中讀取出flag的值爲true放在了工做內存中,而後while去判斷該值是否爲true若是爲true就會不斷的循環,直至flag的值爲false爲止,而後在main方法的線程中修改了demo實例中的flag的值,但demo-thread線程並無感知到flag的值已經被main線程修改成true了,從而發生了沒法中止的現象,簡單點來講這就是線程之間的可見性問題,簡單的畫個圖加深下理解。c++

image-20180818125038462

那麼how to 解決呢?就是咱們這篇的主角volatile(實際上解決的方式有多種,我這裏是爲了寫這篇文章因此這樣解決)git

public class Test {
    public static void main(String[] args) throws InterruptedException {
        ...................
    }

    static class Demo extends Thread{
        volatile boolean flag = true;
        ...........
    }
}

經過volatile關鍵字來修飾該變量,使得該變量的修改是對其它線程可見的,同時volatile還會禁止指令優化的重排序在修飾完volatile後會在對該變量執行操做時插入內存屏障,在進入下一個議題以前先說說何爲內存屏障。github

內存屏障

先說說處理器的內存屏障,再來討論下JVM定義的內存屏障,首先理解內存屏障本質是什麼,在本質上的內存屏障實際上就是一類同步屏障指令,加了屏障的地方,假如屏障前有讀寫操做以及屏障後也有讀寫操做,那麼屏障前的讀寫操做必然必須先於屏障後的讀寫操做,屏障後的讀寫操做也必然必須後於屏障前的讀寫操做(這裏若是理解了亂序執行應該很好理解)windows

處理器的內存屏障緩存

  • read memory barrier (內存讀屏障)

    保障早於屏障以前的讀操做以後再執行晚於屏障的讀操做sass

  • write memory barrier(內存寫屏障)

    保障早於屏障以前的寫操做以後再執行晚於屏障的寫操做多線程

  • full memory barrier(徹底內存屏障)

    保障早於屏障以前的讀寫操做以後再執行晚於屏障以後的讀寫操做

先來認識幾個語義的指令,由於待會在JVM的實現中也會發現它的身影,這裏簡單闡述一下

acquire: 屏障前的指令不會被排到屏障後去

release: 屏障後的指令不會被排到屏障前去

fence:屏障前的指令不會被排到屏障後去,屏障後的指令也不會排到屏障前去

JVM的內存屏障

LoadLoad 讀屏障:例若有指令Load1Load2那麼假如插入屏障指令爲Load1;LoadLoad;Load2,在中間插入LoadLoad屏障能夠保障讀操做不會進行亂序優化,即Load2在執行時,Load1的讀操做應是執行完了的。

StoreStore 寫屏障:例若有指令Store1Store2那麼假如插入屏障指令爲Store1;StoreSotre;Store2,在中間插入StoreStore屏障能夠保障寫操做不會進行亂序優化,即Store2在執行時,Store1的寫操做應是執行完了的,而且Sotre1的寫操做是對Store2可見的,至於爲何可見,我會在說完這四種屏障時給出答案。

LoadStore 讀寫屏障:例若有指令Load1Store2那麼假如插入屏障指令爲Load1;LoadStore;Store2,在中間插入LoadStore屏障能夠保障前面的讀操做和後面的寫操做不會被亂序優化,即Store2執行時,Load1應是執行完了的

StoreLoad 寫讀屏障:例若有指令Store1Load2那麼假如插入屏障的指令爲Store1;StoreLoad;Load2,在中間插入StoreLoad屏障能夠保障前面的寫操做對後面的讀操做不會被亂序優化,而且是可見的,即Load2執行時,Store1應是執行完了的,並其寫操做對屏障後的讀操做可見

若是不管是加了何種屏障,例如 LoadStore屏障即屏障前的讀操做都會從主存中讀取值,屏障後的指令會往主存寫值,再例如 StoreLoad屏障即屏障前的寫操做會往主存寫值,從而對屏障後的讀操做可見,固然僅僅是往主存寫值也不能保障就是可見的,因此後面的讀操做也是從主存中讀值

JVM的實現

So,如今是否是對volatile很清晰了,也知道上面爲何加了volatile關鍵字後,變量是對其它線程可見的了吧?,從而使得咱們的flag修改後可以在多線程環境下可以有效,以一個很是簡單的例子,深刻的探討下去。

public class Demo{
    static volatile int i;
    public static void main(String[] args){
        i = 1;
    }
}

查看生成的字節碼(部分片斷)

static volatile int i;
    descriptor: I
    flags: ACC_STATIC, ACC_VOLATILE

能夠看到在字節碼文件上有一個ACC_VOLATILE的標識符,接着打開JVM(我用的Hotspot)的代碼來看看吧~

能夠在JVM源碼中看到有一個is_volatile判斷是不是volatile訪問限定符修飾的,而後再看字節碼解釋器的部分源碼

注意有三個細節,首先會判斷是否標識了volatile,而後再判斷類型,咱們這個是int類型因此會調用release_int_field_put,最後插入一道屏障storeload,首先先看itos的定義

顧名思義:表示棧頂緩存的int類型數據

接着看release_int_field_put

會發現它調用了OrderAccess::release_store

那麼這個方法到底是幹什麼的呢?首先注意方法參數添加了volatile關鍵字,這是c++的volatile關鍵字和Java(java語法也有這個同名的關鍵字不是嗎?)被該關鍵字所修飾的變量意味着易變的,再c++中修飾了這個關鍵字的變量每次使用時都會從變量對應的內存地址且編譯器也不會對它進行優化。

那麼os::atomic_copy64又是什麼呢?這裏會針對不一樣的系統,我這裏只看Linux的

粗暴點,就是生成彙編代碼去拷貝值吧?

接着咱們看

接着看OrderAccess::storeload

請告訴我這4個東西眼熟不眼熟?!?,這固然只是定義不一樣系統下實現不同,咱們這裏仍是看linux

看這個方法的實現,還有其它三種的實現是什麼?本篇文章上面也對該語義進行了闡述吧?繼續看linux下的實現

FULL_MEM_BARRIER是什麼本文上面也說了吧

針對的環境不通實現也不一樣,這裏具體就再也不闡述

生成的彙編指令

= = 寫到這個小節的時候筆者是用的windows了,因此再補一下windows對fence實現的源碼

看下在我機器上生成的彙編代碼

[Disassembling for mach='amd64']
[Entry Point]
[Verified Entry Point]
[Constants]
  # {method} {0x0000000017cf2a38} 'main' '([Ljava/lang/String;)V' in 'org/yuequan/thread/test/Demo'
  # parm0:    rdx:rdx   = '[Ljava/lang/String;'
  #           [sp+0x40]  (sp of caller)
  0x00000000037e5320: mov     dword ptr [rsp+0ffffffffffffa000h],eax
  0x00000000037e5327: push    rbp
  0x00000000037e5328: sub     rsp,30h
  0x00000000037e532c: mov     rsi,17cf2af8h     ;   {metadata(method data for {method} {0x0000000017cf2a38} 'main' '([Ljava/lang/String;)V' in 'org/yuequan/thread/test/Demo')}
  0x00000000037e5336: mov     edi,dword ptr [rsi+0dch]
  0x00000000037e533c: add     edi,8h
  0x00000000037e533f: mov     dword ptr [rsi+0dch],edi
  0x00000000037e5345: mov     rsi,17cf2a30h     ;   {metadata({method} {0x0000000017cf2a38} 'main' '([Ljava/lang/String;)V' in 'org/yuequan/thread/test/Demo')}
  0x00000000037e534f: and     edi,0h
  0x00000000037e5352: cmp     edi,0h
  0x00000000037e5355: je      37e537eh          ;*iconst_1
                                                ; - org.yuequan.thread.test.Demo::main@0 (line 6)

  0x00000000037e535b: mov     rsi,0d5b0dad0h    ;   {oop(a 'java/lang/Class' = 'org/yuequan/thread/test/Demo')}
  0x00000000037e5365: mov     edi,1h
  0x00000000037e536a: mov     dword ptr [rsi+68h],edi
  0x00000000037e536d: lock add dword ptr [rsp],0h  ;*putstatic i
                                                ; - org.yuequan.thread.test.Demo::main@1 (line 6)

  0x00000000037e5372: add     rsp,30h
  0x00000000037e5376: pop     rbp
  0x00000000037e5377: test    dword ptr [2f20100h],eax
                                                ;   {poll_return}
  0x00000000037e537d: ret
  0x00000000037e537e: mov     qword ptr [rsp+8h],rsi
  0x00000000037e5383: mov     qword ptr [rsp],0ffffffffffffffffh
  0x00000000037e538b: call    37e20a0h          ; OopMap{rdx=Oop off=112}
                                                ;*synchronization entry
                                                ; - org.yuequan.thread.test.Demo::main@-1 (line 6)
                                                ;   {runtime_call}
  0x00000000037e5390: jmp     37e535bh
  0x00000000037e5392: nop
  0x00000000037e5393: nop
  0x00000000037e5394: mov     rax,qword ptr [r15+2a8h]
  0x00000000037e539b: mov     r10,0h
  0x00000000037e53a5: mov     qword ptr [r15+2a8h],r10
  0x00000000037e53ac: mov     r10,0h
  0x00000000037e53b6: mov     qword ptr [r15+2b0h],r10
  0x00000000037e53bd: add     rsp,30h
  0x00000000037e53c1: pop     rbp
  0x00000000037e53c2: jmp     374ece0h          ;   {runtime_call}
  0x00000000037e53c7: hlt
  0x00000000037e53c8: hlt
  0x00000000037e53c9: hlt
  0x00000000037e53ca: hlt
  0x00000000037e53cb: hlt
  0x00000000037e53cc: hlt
  0x00000000037e53cd: hlt
  0x00000000037e53ce: hlt
  0x00000000037e53cf: hlt
  0x00000000037e53d0: hlt
  0x00000000037e53d1: hlt
  0x00000000037e53d2: hlt
  0x00000000037e53d3: hlt
  0x00000000037e53d4: hlt
  0x00000000037e53d5: hlt
  0x00000000037e53d6: hlt
  0x00000000037e53d7: hlt
  0x00000000037e53d8: hlt
  0x00000000037e53d9: hlt
  0x00000000037e53da: hlt
  0x00000000037e53db: hlt
  0x00000000037e53dc: hlt
  0x00000000037e53dd: hlt
  0x00000000037e53de: hlt
  0x00000000037e53df: hlt
[Exception Handler]
[Stub Code]
  0x00000000037e53e0: call    3750aa0h          ;   {no_reloc}
  0x00000000037e53e5: mov     qword ptr [rsp+0ffffffffffffffd8h],rsp
  0x00000000037e53ea: sub     rsp,80h
  0x00000000037e53f1: mov     qword ptr [rsp+78h],rax
  0x00000000037e53f6: mov     qword ptr [rsp+70h],rcx
  0x00000000037e53fb: mov     qword ptr [rsp+68h],rdx
  0x00000000037e5400: mov     qword ptr [rsp+60h],rbx
  0x00000000037e5405: mov     qword ptr [rsp+50h],rbp
  0x00000000037e540a: mov     qword ptr [rsp+48h],rsi
  0x00000000037e540f: mov     qword ptr [rsp+40h],rdi
  0x00000000037e5414: mov     qword ptr [rsp+38h],r8
  0x00000000037e5419: mov     qword ptr [rsp+30h],r9
  0x00000000037e541e: mov     qword ptr [rsp+28h],r10
  0x00000000037e5423: mov     qword ptr [rsp+20h],r11
  0x00000000037e5428: mov     qword ptr [rsp+18h],r12
  0x00000000037e542d: mov     qword ptr [rsp+10h],r13
  0x00000000037e5432: mov     qword ptr [rsp+8h],r14
  0x00000000037e5437: mov     qword ptr [rsp],r15
  0x00000000037e543b: mov     rcx,6601c4e0h     ;   {external_word}
  0x00000000037e5445: mov     rdx,37e53e5h      ;   {internal_word}
  0x00000000037e544f: mov     r8,rsp
  0x00000000037e5452: and     rsp,0fffffffffffffff0h
  0x00000000037e5456: call    65cd4510h         ;   {runtime_call}
  0x00000000037e545b: hlt
[Deopt Handler Code]
  0x00000000037e545c: mov     r10,37e545ch      ;   {section_word}
  0x00000000037e5466: push    r10
  0x00000000037e5468: jmp     3727600h          ;   {runtime_call}
  0x00000000037e546d: hlt
  0x00000000037e546e: hlt
  0x00000000037e546f: hlt
Decoding compiled method 0x00000000037e4ed0:
Code:
Argument 0 is unknown.RIP: 0x37e5020 Code size: 0x00000110
[Entry Point]
[Verified Entry Point]
[Constants]
  # {method} {0x0000000017cf2a38} 'main' '([Ljava/lang/String;)V' in 'org/yuequan/thread/test/Demo'
  # parm0:    rdx:rdx   = '[Ljava/lang/String;'
  #           [sp+0x40]  (sp of caller)
  0x00000000037e5020: mov     dword ptr [rsp+0ffffffffffffa000h],eax
  0x00000000037e5027: push    rbp
  0x00000000037e5028: sub     rsp,30h           ;*iconst_1
                                                ; - org.yuequan.thread.test.Demo::main@0 (line 6)

  0x00000000037e502c: mov     rsi,0d5b0dad0h    ;   {oop(a 'java/lang/Class' = 'org/yuequan/thread/test/Demo')}
  0x00000000037e5036: mov     edi,1h
  0x00000000037e503b: mov     dword ptr [rsi+68h],edi
  0x00000000037e503e: lock add dword ptr [rsp],0h  ;*putstatic i
                                                ; - org.yuequan.thread.test.Demo::main@1 (line 6)

  0x00000000037e5043: add     rsp,30h
  0x00000000037e5047: pop     rbp
  0x00000000037e5048: test    dword ptr [2f20100h],eax
                                                ;   {poll_return}
  0x00000000037e504e: ret
  0x00000000037e504f: nop
  0x00000000037e5050: nop
  0x00000000037e5051: mov     rax,qword ptr [r15+2a8h]
  0x00000000037e5058: mov     r10,0h
  0x00000000037e5062: mov     qword ptr [r15+2a8h],r10
  0x00000000037e5069: mov     r10,0h
  0x00000000037e5073: mov     qword ptr [r15+2b0h],r10
  0x00000000037e507a: add     rsp,30h
  0x00000000037e507e: pop     rbp
  0x00000000037e507f: jmp     374ece0h          ;   {runtime_call}
  0x00000000037e5084: hlt
  0x00000000037e5085: hlt
  0x00000000037e5086: hlt
  0x00000000037e5087: hlt
  0x00000000037e5088: hlt
  0x00000000037e5089: hlt
  0x00000000037e508a: hlt
  0x00000000037e508b: hlt
  0x00000000037e508c: hlt
  0x00000000037e508d: hlt
  0x00000000037e508e: hlt
  0x00000000037e508f: hlt
  0x00000000037e5090: hlt
  0x00000000037e5091: hlt
  0x00000000037e5092: hlt
  0x00000000037e5093: hlt
  0x00000000037e5094: hlt
  0x00000000037e5095: hlt
  0x00000000037e5096: hlt
  0x00000000037e5097: hlt
  0x00000000037e5098: hlt
  0x00000000037e5099: hlt
  0x00000000037e509a: hlt
  0x00000000037e509b: hlt
  0x00000000037e509c: hlt
  0x00000000037e509d: hlt
  0x00000000037e509e: hlt
  0x00000000037e509f: hlt
[Exception Handler]
[Stub Code]
  0x00000000037e50a0: call    3750aa0h          ;   {no_reloc}
  0x00000000037e50a5: mov     qword ptr [rsp+0ffffffffffffffd8h],rsp
  0x00000000037e50aa: sub     rsp,80h
  0x00000000037e50b1: mov     qword ptr [rsp+78h],rax
  0x00000000037e50b6: mov     qword ptr [rsp+70h],rcx
  0x00000000037e50bb: mov     qword ptr [rsp+68h],rdx
  0x00000000037e50c0: mov     qword ptr [rsp+60h],rbx
  0x00000000037e50c5: mov     qword ptr [rsp+50h],rbp
  0x00000000037e50ca: mov     qword ptr [rsp+48h],rsi
  0x00000000037e50cf: mov     qword ptr [rsp+40h],rdi
  0x00000000037e50d4: mov     qword ptr [rsp+38h],r8
  0x00000000037e50d9: mov     qword ptr [rsp+30h],r9
  0x00000000037e50de: mov     qword ptr [rsp+28h],r10
  0x00000000037e50e3: mov     qword ptr [rsp+20h],r11
  0x00000000037e50e8: mov     qword ptr [rsp+18h],r12
  0x00000000037e50ed: mov     qword ptr [rsp+10h],r13
  0x00000000037e50f2: mov     qword ptr [rsp+8h],r14
  0x00000000037e50f7: mov     qword ptr [rsp],r15
  0x00000000037e50fb: mov     rcx,6601c4e0h     ;   {external_word}
  0x00000000037e5105: mov     rdx,37e50a5h      ;   {internal_word}
  0x00000000037e510f: mov     r8,rsp
  0x00000000037e5112: and     rsp,0fffffffffffffff0h
  0x00000000037e5116: call    65cd4510h         ;   {runtime_call}
  0x00000000037e511b: hlt
[Deopt Handler Code]
  0x00000000037e511c: mov     r10,37e511ch      ;   {section_word}
  0x00000000037e5126: push    r10
  0x00000000037e5128: jmp     3727600h          ;   {runtime_call}
  0x00000000037e512d: hlt
  0x00000000037e512e: hlt
  0x00000000037e512f: hlt

這麼長腫麼看?看關鍵部位就好拉

看到是使用的lock指令吧?那麼問題來了lock指令是什麼,我在這裏膚淺的解釋一下:CPU提供了在執行指令期間提供了總線加鎖的手段,那麼加了lock的彙編生成機器碼就使CPU在執行這條指令的時候會把#HLOCK pin的電位拉低,持續到這條指令結束時放開,從而把總線鎖住,從而保證這條指令執行的原子性

如何保障的的可見性和有序性

經過內存屏障來提醒編譯器和CPU不對指令進行優化防止其優化亂序執行從而達到有序性,在屏障的先後都是經過主存讀寫達到線程之間的可見性。(O(∩_∩)O 我想不用再多解釋了)

爲何volatile不能保證複合操做的原子性

就好比在多線程中,多個線程對於一個實例變量的變量i進行自增操做,例如i++,這時候就產生了競態條件致使,例如給i變量加5000次,獲得的結果多是5000獲得的結果也多是少於5000,儘管你加了volatile,這是爲何呢?要知道volatile僅是經過內存屏障的機制來保障

例如

load1;load2;store1;store2;StoreLoad;load3;store3.....

儘管你保證了可見性,但你不能保證該操做的原子性,所謂的原子性本質就是指令在執行期間不被打斷,要麼就是不執行,仔細想一想i++是否是一個三步的複合操做:取值、相加、賦值,就好比:你在未賦值時別的線程執行時,別的線程也正在處以賦值狀態,語言解釋好麻煩看下列示例

i= 0
線程A    線程B
取值 0
         取值 0
相加 1   
         相加 1
賦值 1   
         賦值 1

儘管你保障了可見性,但你並不能保證你拿到手上的值永遠是最新值。

相關文章
相關標籤/搜索