Volatile關鍵字對熟悉java多線程的朋友來講,應該很熟悉了。Volatile是JMM(Java Memory Model)的一個很是重要的關鍵詞。經過是用Volatile能夠實現禁止重排序和變量值線程之間可見兩個主要特性。java
今天咱們從彙編的角度來分析一下Volatile關鍵字究竟是怎麼工做的。多線程
這個世界上有兩種重排序的方式。架構
第一種,是在編譯器級別的,你寫一個java源代碼,通過javac編譯以後,生成的字節碼順序可能跟源代碼的順序不一致。jvm
第二種,是硬件或者CPU級別的重排序,爲了充分利用多核CPU的性能,或者CPU自身的處理架構(好比cache line),可能會對代碼進行重排序。好比同時加載兩個非互相依賴的字段進行處理,從而提高處理速度。oop
咱們舉個例子:性能
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字段,而後在循環中對這些字段進行累加。spa
先看下javac編譯出來的字節碼的順序:線程
咱們能夠看到在設置值的過程當中是和java源代碼的順序是一致的,是按照int1,int2,int3,int4,int5的順序一個一個設置的。指針
而後咱們看一下生成的彙編語言代碼:code
在運行是添加參數-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:-Inline,或者直接使用JIT Watcher。
從生成的代碼中,咱們能夠看到putstatic是按照int1,int5,int4,int3,int2的順序進行的,也就是說進行了重排序。
若是咱們將int2設置成爲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編譯器:
此次結果沒錯,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,可能對性能會有影響。
上面咱們提到了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; } }
從結果能夠看到,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次。看下編譯結果:
從結果能夠看出,getstatic int1和不使用volatile關鍵字,生成的代碼是同樣的。
因此volatile對讀的性能不會產生影響。
本文從彙編語言的角度再次深刻探討了volatile關鍵字和JMM模型的影響,但願你們可以喜歡。
本文做者:flydean程序那些事本文連接:http://www.flydean.com/jvm-volatile-assembly/
本文來源:flydean的博客
歡迎關注個人公衆號:程序那些事,更多精彩等着您!