java指令重排序的問題

指令重排序是個比較複雜、以爲有些難以想象的問題,一樣是先以例子開頭(建議你們跑下例子,這是實實在在能夠重現的,重排序的機率仍是挺高的),有個感性的認識編程

/**
 * 一個簡單的展現Happen-Before的例子.
 * 這裏有兩個共享變量:a和flag,初始值分別爲0和false.在ThreadA中先給     a=1,而後flag=true.
 * 若是按照有序的話,那麼在ThreadB中若是if(flag)成功的話,則應該a=1,而a=a*1以後a仍然爲1,下方的if(a==0)應該永遠不會爲
 * 真,永遠不會打印.
 * 但實際狀況是:在試驗100次的狀況下會出現0次或幾回的打印結果,而試驗1000次結果更明顯,有十幾回打印.
 */
public class SimpleHappenBefore {
    /** 這是一個驗證結果的變量 */
    private static int a=0;
    /** 這是一個標誌位 */
    private static boolean flag=false;
    public static void main(String[] args) throws InterruptedException {
        //因爲多線程狀況下未必會試出重排序的結論,因此多試一些次
        for(int i=0;i<1000;i++){
            ThreadA threadA=new ThreadA();
            ThreadB threadB=new ThreadB();
            threadA.start();
            threadB.start();
            //這裏等待線程結束後,重置共享變量,以使驗證結果的工做變得簡單些.
            threadA.join();
            threadB.join();
            a=0;
            flag=false;
        }
    }
    static class ThreadA extends Thread{
        public void run(){
        a=1;
        flag=true;
        }
    }
    static class ThreadB extends Thread{
        public void run(){
            if(flag){
            a=a*1;
            }
            if(a==0){
            System.out.println("ha,a==0");
            }
        }
    }
}

 

例子比較簡單,也添加了註釋,再也不詳細敘述。
什麼是指令重排序?有兩個層面:
在虛擬機層面,爲了儘量減小內存操做速度遠慢於CPU運行速度所帶來的CPU空置的影響,虛擬機會按照本身的一些規則(這規則後面再敘述)將程序編寫順序打亂——即寫在後面的代碼在時間順序上可能會先執行,而寫在前面的代碼會後執行——以儘量充分地利用CPU。拿上面的例子來講:假如不是a=1的操做,而是a=new byte[1024*1024](分配1M空間)`,那麼它會運行地很慢,此時CPU是等待其執行結束呢,仍是先執行下面那句flag=true呢?顯然,先執行flag=true能夠提早使用CPU,加快總體效率,固然這樣的前提是不會產生錯誤(什麼樣的錯誤後面再說)。雖然這裏有兩種狀況:後面的代碼先於前面的代碼開始執行;前面的代碼先開始執行,但當效率較慢的時候,後面的代碼開始執行並先於前面的代碼執行結束。無論誰先開始,總以後面的代碼在一些狀況下存在先結束的可能。
在硬件層面,CPU會將接收到的一批指令按照其規則重排序,一樣是基於CPU速度比緩存速度快的緣由,和上一點的目的相似,只是硬件處理的話,每次只能在接收到的有限指令範圍內重排序,而虛擬機能夠在更大層面、更多指令範圍內重排序。硬件的重排序機制參見《從JVM併發看CPU內存指令重排序(Memory Reordering)》
重排序很很差理解,上面只是簡單地提了下其場景,要想較好地理解這個概念,須要構造一些例子和圖表,在這裏介紹兩篇介紹比較詳細、生動的文章《happens-before俗解》和《深刻理解Java內存模型(二)——重排序》。其中的「as-if-serial」是應該掌握的,即:無論怎麼重排序,單線程程序的執行結果不能被改變。編譯器、運行時和處理器都必須遵照「as-if-serial」語義。拿個簡單例子來講,緩存

public void execute(){
    int a=0;
    int b=1;
    int c=a+b;
}

 

 

這裏a=0,b=1兩句能夠隨便排序,不影響程序邏輯結果,但c=a+b這句必須在前兩句的後面執行。
從前面那個例子能夠看到,重排序在多線程環境下出現的機率仍是挺高的,在關鍵字上有volatile和synchronized能夠禁用重排序,除此以外還有一些規則,也正是這些規則,使得咱們在平時的編程工做中沒有感覺到重排序的壞處。
程序次序規則(Program Order Rule):在一個線程內,按照代碼順序,書寫在前面的操做先行發生於書寫在後面的操做。準確地說應該是控制流順序而不是代碼順序,由於要考慮分支、循環等結構。
監視器鎖定規則(Monitor Lock Rule):一個unlock操做先行發生於後面對同一個對象鎖的lock操做。這裏強調的是同一個鎖,而「後面」指的是時間上的前後順序,如發生在其餘線程中的lock操做。
volatile變量規則(Volatile Variable Rule):對一個volatile變量的寫操做發生於後面對這個變量的讀操做,這裏的「後面」也指的是時間上的前後順序。
線程啓動規則(Thread Start Rule):Thread獨享的start()方法先行於此線程的每個動做。
線程終止規則(Thread Termination Rule):線程中的每一個操做都先行發生於對此線程的終止檢測,咱們能夠經過Thread.join()方法結束、Thread.isAlive()的返回值檢測到線程已經終止執行。
線程中斷規則(Thread Interruption Rule):對線程interrupte()方法的調用優先於被中斷線程的代碼檢測到中斷事件的發生,能夠經過Thread.interrupted()方法檢測線程是否已中斷。
對象終結原則(Finalizer Rule):一個對象的初始化完成(構造函數執行結束)先行發生於它的finalize()方法的開始。
傳遞性(Transitivity):若是操做A先行發生於操做B,操做B先行發生於操做C,那就能夠得出操做A先行發生於操做C的結論。
正是以上這些規則保障了happen-before的順序,若是不符合以上規則,那麼在多線程環境下就不能保證執行順序等同於代碼順序,也就是「若是在本線程中觀察,全部的操做都是有序的;若是在一個線程中觀察另一個線程,則不符合以上規則的都是無序的」,所以,若是咱們的多線程程序依賴於代碼書寫順序,那麼就要考慮是否符合以上規則,若是不符合就要經過一些機制使其符合,最經常使用的就是synchronized、Lock以及volatile修飾符。多線程

相關文章
相關標籤/搜索