什么是亂序執行
CPU運行的時候,是按照指令一條一條執行的。CPU速度特別快,但是CPU從內存去取數據的話,會很慢。這時候,就可能出現后來的指令要比先到的指令先執行的情況,例如:現在給CPU兩條指令 ,兩個指令沒有關系。第一條指令從內存讀數據。需要等待很長時間,那么在等待內存的過程中,會先執行指令2。然后等數據到了以后,然后執行指令1。看起來就是指令2 比指令1先執行。這些亂序執行,是CPU自動優化的結果,可以提高效率。而且如果第二條指令依賴第一條指令,不會發生指令亂序執行。
亂序執行的測試代碼:
/** * 指令重排序驗證,在這個案例代碼中,可能出現場景如下: * 1.one線程執行完畢后執行other 線程結果:a=1,x=0;b=1,y=1; xy值為:01組合 * 2.other線程執行完畢后執行one 線程結果:a=1,x=1;b=1,y=0; xy值為:10組合 * 3.other線程和one線程同時執行 線程結果:a=1,x=1;b=1,y=1; xy值為:11組合 * 按照正常邏輯,不可能出現 xy值為00的組合 * * * 出現這種情況,只有一種可能,出現亂序執行了。 * @author LYs * */ public class T04_Disorder { private static int x = 0,y=0; private static int a= 0,b=0; public static void main(String[] args) throws Exception { int i=0; for (; ;) { i++; x=0;y=0; a=0;b=0; Thread one = new Thread(new Runnable() { @Override public void run() { a=1; x=b; } }); Thread other = new Thread(new Runnable() { @Override public void run() { b=1; y=a; } }); one.start();other.start(); one.join();other.join(); String result = "第"+i+"次("+x+","+y+")"; if (x==0 && y==0) { System.err.println(result); break; } } } }
亂序執行有可能帶來的問題
有時候亂序執行會導致一些問題,單線程情況下,好像基本上沒有啥影響。但是如果是多線程的話,就有可能會導致一些問題出現。舉例:我們在創建對象的時候, new方法會產生4條指令。舉例說明:
java代碼
public class Test { int i = 8; }
指令如下:
1 NEW Test2 DUP 3 INVOKESPECIAL Test.<init>()V 4 ASTORE 1
其中第一行是在內存中為對象分配一塊空間,這時候int i 的值是默認值0 。第二行是將指針緩存下這個不需要理解不影響分析亂序執行問題。第三行指令執行操作,將8賦值給變量i 。第四行指令將該內存的對象引用賦值給棧內的引用。當有兩個線程,線程1創建test對象,同時線程2訪問該對象,如果不為空,就取出i的值去進行操作。這時如果指令第三行和第四行發生重排序就會出現問題:
圖片來源於馬士兵線程課
如圖所示,在線程1創建對象並給i默認值為0的時候,將線程的對象引用賦值給了棧中的對象變量。這時,在對該對象做非空判斷時得到的結果是該對象存在。線程2就會去取線程1new出來的對象,但是線程1並沒有執行指令4 init的方法。線程2 取到的值為0 但是正常操作應該取到的值為8 這就是指令重排序帶來的問題。
指令重排序問題解決
指令重排序問題的解決辦法是內存屏障,實現方式為:在執行的時候,加一堵牆,牆兩邊的指令不允許相互之間發生重排序。內存屏障的實現是在CPU級別實現的。Intel硬件提供了一系列的內存屏障,主要有:
1. lfence,是一種Load Barrier 讀屏障
2. sfence, 是一種Store Barrier 寫屏障
3. mfence, 是一種全能型的屏障,具備ifence和sfence的能力
4. Lock前綴,Lock不是一種內存屏障,但是它能完成類似內存屏障的功能。Lock會對CPU總線和高速緩存加鎖,可以理解為CPU指令級的一種鎖。
java代碼想要實現內存屏障,只需要在相應的變量前添加volatile關鍵字。這個關鍵字的作用為:1.保證可見性,2.防止指令重排序。jvm實現防止內存重排序使用的辦法是鎖定cpu總線,該辦法效率較低,但是兼容性比較好。