精確解釋java的volatile之可見性、原子性、有序性(經過彙編語言)

1、實驗環境:html

一、Idea代碼編輯器java

二、jdk1.8.0_92linux

三、win10_x64macos

 

2、易產生誤解的Java字段Volatilewindows

volatile保證了可見性,可是並不保證原子性!!!緩存

1.volatile關鍵字的兩層語義安全

  一旦一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾以後,那麼就具有了兩層語義:多線程

  1)保證了不一樣線程對這個變量進行操做時的可見性,即一個線程修改了某個變量的值,這新值對其餘線程來講是當即可見的。jvm

  2)禁止進行指令重排序。編輯器

 

volatile的可見性,即任什麼時候刻只要有任何線程修改了volatile變量的值,其餘線程總能獲取到該最新值。具體更多實現能夠參閱緩存一致性協議。

 

2.那麼volatile爲何又不能保證原子性呢?

以volatile int i = 10;i++;爲例分析:

i++實際爲load、Increment、store三個操做。

某一時刻線程1將i的值load取出來,放置到cpu緩存中,而後再將此值放置到寄存器A中,而後A中的值自增1(寄存器A中保存的是中間值,沒有直接修改i,所以其餘線程並不會獲取到這個自增1的值)。若是在此時線程2也執行一樣的操做,獲取值i==10,自增1變爲11,而後立刻刷入主內存。此時因爲線程2修改了i的值,實時的線程1中的i==10的值緩存失效,從新從主內存中讀取,變爲11。接下來線程1恢復。將自增事後的A寄存器值11賦值給cpu緩存i。這樣就出現了線程安全問題。

 

3.synchronized相對於volatile又是如何保證原子性呢?

volatile:從最終彙編語言從面來看,volatile使得每次將i進行了修改以後,增長了一個內存屏障lock addl $0x0,(%rsp)保證修改的值必須刷新到主內存才能進行內存屏障後續的指令操做。可是內存屏障以前的指令並非原子的。

synchronized:則是使用lock cmpxchg %rsi,(%rdi)的原子指令,使得修改是原子操做。若是修改失敗,則繼續嘗試,知道成功。

 

3、Java字節碼與彙編語言關係(解釋性語言仍是編譯語言?)

首先咱們簡要解釋下java語言應該是編譯成字節碼、爲何會和彙編語言有聯繫?

現在,基於物理機、虛擬機等的語言,大多都遵循這種基於現代經典編譯原理的思路,在執行前先對程序源碼進行詞法解析和語法解析處理,把源碼轉化爲抽象語法樹。對於一門具體語言的實現來講,詞法和語法分析乃至後面的優化器和目標代碼生成器均可以選擇獨立於執行引擎,造成一個完整意義的編譯器去實現,這類表明是C/C++語言。也能夠把抽象語法樹或指令流以前的步驟實現一個半獨立的編譯器,這類表明是Java語言。又或者能夠把這些步驟和執行引擎所有集中在一塊兒實現,如大多數的JavaScript執行器。

Java便是解釋性語言,又是編譯器語言。Java支持兩種方式同時進行。因爲解釋性語言性能相對較慢,所以Java用了JIT技術,將頻繁執行的代碼編譯成本地機器語言,這樣後續既能夠直接運行。所以使用JIT技術可以獲取到機器彙編語言。

 

4、直接從彙編入手分析volatile及synchronized多線程問題

下述經過四種方式代碼的彙編指令比較,下面針對關於線程安全相關的彙編指令進行重點分析。

PS:具體關於如何獲取彙編代碼,及4種方式java源代碼,請參考第五章。

 

一、普通方式int i,執行i++:

 

普通方式沒有任何與鎖有關的指令;其餘方式都出現了與鎖相關的彙編指令lock。

解釋指令:其中edi爲32位寄存器。若是是long則爲64位的rdi寄存器。

 

 

二、volatile方式volatile int i,執行i++:

 

指令「lock; addl $0,0(%%esp)」表示加鎖,把0加到棧頂的內存單元,該指令操做自己無心義,但這些指令起到內存屏障的做用,讓前面的指令執行完成。具備XMM2特徵的CPU已有內存屏障指令,就直接使用該指令。

volatile字節碼爲:

 

內存屏障有兩個能力:

1. 阻止屏障兩邊的指令重排序

2. 強制把寫緩衝區/高速緩存中的髒數據等寫回主內存,讓緩存中相應的數據失效

 

對Load Barrier來講,在讀指令前插入讀屏障,可讓高速緩存中的數據失效,從新從主內存加載數據

對Store Barrier來講,在寫指令以後插入寫屏障,能讓寫入緩存的最新數據寫回到主內存。

 

關於原子性解釋:

上述volatile方式的i++,總共是四個步驟:

Load、Increment、Store、Memory Barriers。

Memory Barriers步驟保證了jvm讓這個最新的變量的值在全部線程可見,也就是最後一步讓全部的CPU內核都得到了最新的值,但中間的幾步(從Load到Increment到Store)是不安全的,中間若是其餘的CPU修改了值將會丟失。

爲何從Load到Increment到Store三個指令不是原子性的,請參考intex對原子指令保證的官方文檔:

 

 

文檔地址:https://software.intel.com/sites/default/files/managed/39/c5/325462-sdm-vol-1-2abcd-3abcd.pdf

 

三、synchronizied方式int i,使用synchronized鎖住i++:

在分析synchronizied時候,因爲彙編代碼比較多,所以先將java代碼編譯成字節碼:

查看test方法字節碼:

 

上述彙編代碼可知,monitorenter與monitorexit包裹了getstatic i及putstatic i,等相關代碼執行指令。中間值得交換採用了原子操做lock cmpxchg %rsi,(%rdi),若是交換成功,則執行goto直接退出當前函數return。若是失敗,執行jne跳轉指令,繼續循環執行,直到成功爲止。

 

jne指令:是一個條件轉移指令。當ZF=0,轉至標號處執行。

cmpxchg指令:比較rsi和目的操做數rdi(第一個操做數)的值,若是相同,ZF標誌被設置,同時源操做數(第二個操做)的值被寫到目的操做數,不然,清ZF標誌爲0,而且把目的操做數的值寫回rsi,則執行jne跳轉指令。。

 

5、獲取四種形式的Java代碼對應的彙編指令

一、建立工程:

 

二、配置jvm參數,使之能輸出彙編語言:

 

 

 

須要添加的JVM參數爲:-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly Test(其中Test爲class類名)

 

三、再次運行:

報錯說明:Could not load hsdis-amd64.dll; library not loadable; PrintAssembly is disabled

 

四、下載hsdis-amd64.dll插件:

https://kenai.com/projects/base-hsdis/downloads(網站上面只提供了linux、macos、Solaris等版本下載)

windows版本的hsdis-amd64.dll插件須要自行build:http://dropzone.nfshost.com/hsdis.htm

 

此處給你們提供已經編譯好的dll文件:

http://pan.baidu.com/s/1bpIIzHd

 

下載完成放置到windows的jdk對應目錄下面:

 

五、繼續運行程序:

 

輸出的參數太多,可使用過濾輸出:

Filtering Output

The -XX:+PrintAssembly option prints everything. If that's too much, drop it and use one of the following options.

Individual methods may be printed:

  • CompileCommand=print,*MyClass.myMethod prints assembly for just one method
  • CompileCommand=option,*MyClass.myMethod,PrintOptoAssembly (debug build only) produces the old print command output
  • CompileCommand=option,*MyClass.myMethod,PrintNMethods produces method dumps

These options accumulate.

If you get no output, use -XX:+PrintCompilation to verify that your method is getting compiled at all.

 

若是隻是但願打印某一個方法的彙編將JVM參數設置爲:

java -server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:-Inline -XX:CompileCommand=print,*Test.test

 

其中:

-Xcomp表示永遠以編譯模式運行(禁止解釋器模式)

-XX:-Inline:禁止內聯優化

 

 

六、對比使用普通變量i及volatile變量的彙編指令對比:

package main.java;

/*
 * 使用匯編語言來驗證volatile
 *
 * @author tantexian<my.oschina.net/tantexian>
 * @since 2016/12/17
 *
 * @params java -server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:-Inline -XX:CompileCommand=print,*Test.test
 *
 */
public class Test {
    static int i = 888;
    //static volatile int i = 888;

    public static void main(String[] args){
        test();
    }

    public static void test() {
        synchronized (Test.class) {
            i++;
        }
    }
}

PS:後續實驗的執行參數都爲:

java -server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:-Inline -XX:CompileCommand=print,*Test.test

 

普通變量執行後結果輸出:

normal.txt

 

 

volatile變量修飾後執行後結果輸出:

volatile.txt

 

經過beyondCompare比較:

 

再次補充實驗下long的指令:將volatile int i修給爲 volatile long i:

 

注意:movabsq不是32位的擴展,是純新增的指令。用來將一個64位的字面值直接存到一個64位寄存器中。由於movq只能將32位的值存入,因此新增了這樣一條指令。rdi與r10爲64位寄存器(r爲64位寄存器前綴,e爲32位寄存器前綴)。即將數字1放置到64位寄存器中。add %r10,%rdi將j+1結果保存在rdi中。getstatic用來獲取類的一個靜態字段值。

 

 

七、再來實驗,test方法增長synchronized關鍵字:

 

synchronized-method.txt

 

 

八、再來實驗,test的i++代碼段使用synchronized關鍵字:

 

synchronized-i++.txt

 

 

 

上述四次實驗彙編代碼打包文件地址:http://pan.baidu.com/s/1nuZIOdj

Java四種條件下彙編指令.rar

 

 

參考引用:

https://wiki.openjdk.java.net/display/HotSpot/PrintAssembly

http://www.cnblogs.com/Mainz/p/3556430.html#

相關文章
相關標籤/搜索