指令重排
談到指令重排,首先來了解一下Java內存模型(JMM)。
JMM的關鍵技術點都是圍繞多線程的原子性、可見性、有序性來創建的。
原子性(Atomicity)
原子性是指一個操做是不可中斷的,即便是在多個線程一塊兒執行的時候,一個操做一旦開始,就不會被其它線程干擾。
可見性(Visibility)
可見性是指當一個線程修改了某一個共享變量的值,其餘線程是否可以當即知道這個修改。
對於串行程序來講可見性問題是不存在的,由於在任何一個操做步驟中修改了某個變量,那麼後續的步驟中,讀取這個變量的值,必定是修改以後的。
可是在並行程序中,若是一個線程修改了某一個全局變量,那麼其餘線程未必能夠立刻知道這個改動。(這裏涉及到編譯器優化重排和硬件優化,這裏不重點講述)
有序性(Ordering)
有序性是指在單線程環境中, 程序是按序依次執行的.
而在多線程環境中, 程序的執行可能由於指令重排而出現亂序。
` class OrderExample {
int a = 0;
boolean flag = false;安全
public void writer() { // 如下兩句執行順序可能會在指令重排等場景下發生變化 a = 1; flag = true; } public void reader() { if (flag) { int i = a + 1; …… } } }`
假設線程A首先執行write()方法,接着線程B執行reader()方法,若是發生指令重排,那個線程B在執行 int i = a + 1;時不必定能看見a已經被賦值爲1了。多線程
指令1 IF ID EX MEN WB
指令2 IF ID EX MEN WB架構
指令的每一步都由不一樣的硬件完成,假設每一步耗時1ms,執行完一條指令需耗時5ms,每條指令都按順序執行,那兩條指令則需10ms。
可是經過流水線在指令1剛執行完IF,執行IF的硬件立馬就開始執行指令2的IF,這樣指令2只須要等1ms,兩個指令執行完只須要6ms,效率會有提高巨大!
因此經過流水線技術,可使得CPU高效執行,當流水線滿載時,全部硬件都有序高效執行,可是一旦中斷,全部硬件設備都會進入一個停頓期,再次滿載
須要幾個週期,所以性能損失會比較大,因此必須想辦法儘可能不讓流水線中斷!
此時,指令重排的重要性就此體現出來。固然,指令重排只是減小中斷的一種技術,實際上,在CPU設計中還會使用更多的軟硬件技術來防止中斷。app
如今來看一下代碼 A=B+C 是怎麼執行的
現有R1,R2,R3三個寄存器,
LW R1,B IF ID EX MEN WB(加載B到R1中)
LW R2,C IF ID EX MEN WB(加載C到R2中)
ADD R3,R2,R1 IF ID × EX MEN WB(R1,R2相加放到R3)
SW A,R3 IF ID x EX MEN WB(把R3 的值保存到變量A)
在ADD指令執行中有個x,表示中斷、停頓,ADD爲何要在這裏停頓一下呢?由於這時C還沒加載到R2中,只能等待,而這個等待使得後邊的全部指令都會停頓一下。
這個停頓能夠避免嗎?固然是能夠的,經過指令重排就能夠實現,再看一下下面的例子:jvm
執行A=B+C;D=E-F;
經過將D=E-F執行的指令順序提早,從而消除因等待加載完畢的時間。
一、LW Rb,B IF ID EX MEN WB
二、LW Rc,C IF ID EX MEN WB
三、LW Re,E IF ID EX MEN WB
四、ADD Ra,Rb,Rc IF ID EX MEN WB
五、LW Rf,F IF ID EX MEN WB
六、SW A,Ra IF ID EX MEN WB
七、SUB Rd,Re,Rf IF ID EX MEN WB
八、SW D,Rd IF ID EX MEN WB
在CPU硬件中斷停頓等待的時候 能夠加載別的數據,更加有效利用資源,節約時間。若是不指令重排則白白等待,效率較低。函數
編譯器優化
主要指jvm層面的, 以下代碼, 在jvm client模式很快就跳出了while循環, 而在server模式下運行, 永遠不會中止
`/**oop
Created by Administrator on 2020/11/19
*/
public class VisibilityTest extends Thread {
private boolean stop;性能
public void run() {
int i = 0;
while (!stop) {
i++;
}
System.out.println("finish loop,i=" + i);
}優化
public void stopIt() {
stop = true;
}線程
public boolean getStop() {
return stop;
}
public static void main(String[] args) throws Exception {
VisibilityTest v = new VisibilityTest();
v.start();
Thread.sleep(1000);
v.stopIt();
Thread.sleep(2000);
System.out.println("finish main");
System.out.println(v.getStop());
}
}`
二者區別在於當jvm運行在-client模式的時候,使用的是一個代號爲C1的輕量級編譯器,而-server模式啓動的虛擬機採用相對重量級,代號爲C2的編譯器. C2比C1編譯器編譯的相對完全,會致使程序啓動慢, 但服務起來以後, 性能更高, 同時有可能帶來可見性問題.
再來看兩個從Java語言規範中摘取的例子, 也是涉及到編譯器優化重排, 這裏再也不作詳細解釋,可查詢相關文檔
例子1中有可能出現r2 = 2 而且 r1 = 1;
例子2中是r2, r5值由於都是=r1.x, 編譯器會使用向前替換, 把r5指向到r2, 最終可能致使r2=r5=0, r4 = 3;
lfence指令讀屏障(Load Barrier),做用是:
保證了lfence先後的Load指令的順序,防止Load重排序
刷新Load Buffer
mfence指令全屏障(Full Barrier),做用是:
保證了mfence先後的Store和Load指令的順序,防止Store和Load重排序
保證了mfence以後的Store指令全局可見以前,mfence以前的Store指令要先全局可見
JVM層級:8個hanppens-before原則 4個內存屏障 (LL LS SL SS)
Happen-Before先行發生規則
若是光靠sychronized和volatile來保證程序執行過程當中的原子性, 有序性, 可見性, 那麼代碼將會變得異常繁瑣.
JMM提供了8個Happen-Before規則來約束數據之間是否存在競爭, 線程環境是否安全, 具體以下:
4個內存屏障 (LL LS SL SS)
LoadLoad屏障:對於這樣的語句Load1; LoadLoad; Load2,在Load2及後續讀取操做要讀取的數據被訪問前,保證Load1要讀取的數據被讀取完畢。
StoreStore屏障:對於這樣的語句Store1; StoreStore; Store2,在Store2及後續寫入操做執行前,保證Store1的寫入操做對其它處理器可見。
LoadStore屏障:對於這樣的語句Load1; LoadStore; Store2,在Store2及後續寫入操做被刷出前,保證Load1要讀取的數據被讀取完畢。
StoreLoad屏障:對於這樣的語句Store1; StoreLoad; Load2,在Load2及後續全部讀取操做執行前,保證Store1的寫入對全部處理器可見。它的開銷是四種屏障中最大的。在大多數處理器的實現中,這個屏障是個萬能屏障,兼具其它三種內存屏障的功能。
as-if-serial
As-if-serial語義的意思是,全部的動做(Action)均可覺得了優化而被重排序,可是必須保證它們重排序後的結果和程序代碼自己的應有結果是一致的。Java編譯器、運行時和處理器都會保證單線程下的as-if-serial語義。