天天看點

JVM系列之:從彙編角度分析Volatile

目錄

  • ​​簡介​​
  • ​​重排序​​
  • ​​寫的記憶體屏障​​
  • ​​非lock和LazySet​​
  • ​​讀的性能​​
  • ​​總結​​

簡介

Volatile關鍵字對熟悉java多線程的朋友來說,應該很熟悉了。Volatile是JMM(Java Memory Model)的一個非常重要的關鍵詞。通過是用Volatile可以實作禁止重排序和變量值線程之間可見兩個主要特性。

今天我們從彙編的角度來分析一下Volatile關鍵字到底是怎麼工作的。

重排序

這個世界上有兩種重排序的方式。

第一種,是在編譯器級别的,你寫一個java源代碼,經過javac編譯之後,生成的位元組碼順序可能跟源代碼的順序不一緻。

第二種,是硬體或者CPU級别的重排序,為了充分利用多核CPU的性能,或者CPU自身的處理架構(比如cache line),可能會對代碼進行重排序。比如同時加載兩個非互相依賴的字段進行處理,進而提升處理速度。

我們舉個例子:

public class TestVolatile {

    private static int int1;
    private static int int2;
    private static int int3;
    private static int int4;
    private static int int5;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10000; i++)
        {
            increase(i);
        }
        Thread.sleep(1000);
    }

    private static void increase(int i){
        int1= i+1;
        int2= i+2;
        int3= i+3;
        int4= i+4;
        int5= i+5;
    }
}      

上面例子中,我們定義了5個int字段,然後在循環中對這些字段進行累加。

先看下javac編譯出來的位元組碼的順序:

JVM系列之:從彙編角度分析Volatile

我們可以看到在設定值的過程中是和java源代碼的順序是一緻的,是按照int1,int2,int3,int4,int5的順序一個一個設定的。

然後我們看一下生成的彙編語言代碼:

在運作是添加參數-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:-Inline,或者直接使用JIT Watcher。
JVM系列之:從彙編角度分析Volatile

從生成的代碼中,我們可以看到putstatic是按照int1,int5,int4,int3,int2的順序進行的,也就是說進行了重排序。

如果我們将int2設定成為Volatile,看看結果如何?

前方高能預警,請小夥伴們做好準備
JVM系列之:從彙編角度分析Volatile

我們先看putstatic的順序,從注釋裡面,我們隻發現了putstatic int2, int3和int5。

且慢!我們不是需要設定int1,int2,int3,int4,int5 5個值嗎?這裡怎麼隻有3個。

要是沒有能獨立思考和獨立決定的有創造個人,社會的向上發展就不可想像 - 愛因斯坦

這裡是反編譯的時候注釋寫錯了!

讓我們來仔細分析一下彙編代碼。

第一個紅框,不用懂彙編語言的朋友應該也可以看懂,就是分别給r11d,r8d,r9d,ecx和esi這5個寄存器分别加1,2,3,4,5。

這也分别對應了我們在increase方法中要做的事情。

有了這些寄存器的值,我們再繼續往下看,進而可以知道,第二個紅框實際上表示的就是putstatic int1,而最後一個紅框,表示的就是putstatic int4。

是以,大家一定要學會自己分析代碼。

5個putstatic都在,同時因為使用了volatile關鍵字,是以int2作為一個分界點,不會被重排序。是以int1一定在int2之前,而int3,4,5一定在int2之後。

上圖的結果是在JIT Watcher中的C2編譯器的結果,如果我們切換到C1編譯器:

JVM系列之:從彙編角度分析Volatile

這次結果沒錯,5個int都在,同時我們看到這5個int居然沒有重排序。

這也說明了不同的編譯器可能對重排序的了解程度是不一樣的。

寫的記憶體屏障

再來分析一下上面的putstatic int2:

lock addl $0x0,-0x40(%rsp)  ;*putstatic int2 {reexecute=0 rethrow=0 return_oop=0}      

這裡使用了 lock addl指令,給rsp加了0。 rsp是SP (Stack Pointer) register,也就是棧指針寄存器。

給rsp加0,是不是很奇怪?

加0,雖然沒有改變rsp的值,但是因為前面加了lock,是以這個指令會被解析為記憶體屏障。

這個記憶體屏障保證了兩個事情,第一,不會重排序。第二,所有的變量值都會回寫到主記憶體中,進而在這個指令之後,變量值對其他線程可見。

當然,因為使用lock,可能對性能會有影響。

非lock和LazySet

上面我們提到了volatile會導緻生成lock指令。

但有時候,我們隻是想阻止重排序,對于變量的可見性并沒有那麼嚴格的要求。

這個時候,我們就可以使用Atomic類中的LazySet:

public class TestVolatile2 {

    private static int int1;
    private static AtomicInteger int2=new AtomicInteger(0);
    private static int int3;
    private static int int4;
    private static int int5;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10000; i++)
        {
            increase(i);
        }
        Thread.sleep(1000);
    }

    private static void increase(int i){
        int1= i+1;
        int2.lazySet(i+2);
        int3= i+3;
        int4= i+4;
        int5= i+5;
    }
}      
JVM系列之:從彙編角度分析Volatile

從結果可以看到,int2沒有重排序,也沒有添加lock。s

注意,上面的最後一個紅框表示的是putstatic int4。

讀的性能

最後,我們看下使用volatile關鍵字對讀的性能影響:

public class TestVolatile3 {

    private static volatile int int1=10;
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10000; i++)
        {
            readInt(i);
        }
        Thread.sleep(1000);
    }

    private static void readInt(int i){
        if(int1 < 5){
            System.out.println(i);
        }
    }
}      

上面的例子中,我們對int1讀取10000次。看下編譯結果:

JVM系列之:從彙編角度分析Volatile

從結果可以看出,getstatic int1和不使用volatile關鍵字,生成的代碼是一樣的。

是以volatile對讀的性能不會産生影響。

總結

本文從彙編語言的角度再次深入探讨了volatile關鍵字和JMM模型的影響,希望大家能夠喜歡。

本文作者:flydean程式那些事