1. 程式人生 > >JVM系列之:從彙編角度分析Volatile

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

[toc] # 簡介 Volatile關鍵字對熟悉java多執行緒的朋友來說,應該很熟悉了。Volatile是JMM(Java Memory Model)的一個非常重要的關鍵詞。通過是用Volatile可以實現禁止重排序和變數值執行緒之間可見兩個主要特性。 今天我們從彙編的角度來分析一下Volatile關鍵字到底是怎麼工作的。 # 重排序 這個世界上有兩種重排序的方式。 第一種,是在編譯器級別的,你寫一個java原始碼,經過javac編譯之後,生成的位元組碼順序可能跟原始碼的順序不一致。 第二種,是硬體或者CPU級別的重排序,為了充分利用多核CPU的效能,或者CPU自身的處理架構(比如cache line),可能會對程式碼進行重排序。比如同時載入兩個非互相依賴的欄位進行處理,從而提升處理速度。 我們舉個例子: ~~~java 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編譯出來的位元組碼的順序: ![](https://img-blog.csdnimg.cn/20200630142134260.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 我們可以看到在設定值的過程中是和java原始碼的順序是一致的,是按照int1,int2,int3,int4,int5的順序一個一個設定的。 然後我們看一下生成的組合語言程式碼: > 在執行是新增引數-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:-Inline,或者直接使用JIT Watcher。 ![](https://img-blog.csdnimg.cn/20200630142202510.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 從生成的程式碼中,我們可以看到putstatic是按照int1,int5,int4,int3,int2的順序進行的,也就是說進行了重排序。 如果我們將int2設定成為Volatile,看看結果如何? >
前方高能預警,請小夥伴們做好準備 ![](https://img-blog.csdnimg.cn/20200630145044404.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 我們先看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編譯器: ![](https://img-blog.csdnimg.cn/20200630151458134.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 這次結果沒錯,5個int都在,同時我們看到這5個int居然沒有重排序。 這也說明了不同的編譯器可能對重排序的理解程度是不一樣的。 # 寫的記憶體屏障 再來分析一下上面的putstatic int2: ~~~java 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: ~~~java 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; } } ~~~ ![](https://img-blog.csdnimg.cn/2020063015351643.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 從結果可以看到,int2沒有重排序,也沒有新增lock。s >
注意,上面的最後一個紅框表示的是putstatic int4。 # 讀的效能 最後,我們看下使用volatile關鍵字對讀的效能影響: ~~~java 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次。看下編譯結果: ![](https://img-blog.csdnimg.cn/20200630153958829.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 從結果可以看出,getstatic int1和不使用volatile關鍵字,生成的程式碼是一樣的。 所以volatile對讀的效能不會產生影響。 # 總結 本文從組合語言的角度再次深入探討了volatile關鍵字和JMM模型的影響,希望大家能夠喜歡。 > 本文作者:flydean程式那些事 > > 本文連結:[http://www.flydean.com/jvm-volatile-assembly/](http://www.flydean.com/jvm-volatile-assembly/) >
> 本文來源:flydean的部落格 > > 歡迎關注我的公眾號:程式那些事,更多精彩等著您!